From 6330445b26f27c58b56d56ea58f768c7c24de4e0 Mon Sep 17 00:00:00 2001 From: Rajpreet Singh <63117988+rajpreet-s@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:57:42 +0530 Subject: [PATCH] 12 hover provider for manifest files (#46) * added policy violations per dependency * added regex method * refactored * code refactor * code refactor * refactored and cleaned * refactored --------- Co-authored-by: Nagarjun Sanji --- src/constants/debricked_cli.ts | 3 +- src/constants/index.ts | 13 ++- src/constants/organization.ts | 1 + src/constants/policyRulesEnum.ts | 13 +++ src/constants/regex.ts | 7 ++ src/extension.ts | 1 + src/helpers/commonHelper.ts | 44 ++++++++++ src/helpers/fileHelper.ts | 34 +++----- src/helpers/globalStore.ts | 24 ++--- src/helpers/index.ts | 2 +- src/helpers/template.ts | 41 ++++----- src/providers/dependencyPolicyProvider.ts | 84 ++++++++++++++++++ src/providers/index.ts | 39 ++++++++- .../manifestDependencyHoverProvider.ts | 87 ++++++++----------- src/services/dependencyService.ts | 69 +++++++++------ src/services/fileService.ts | 59 ++++++++++++- src/services/scanService.ts | 13 +-- src/types/index.ts | 24 ++++- src/types/package.ts | 17 ++++ src/types/scannedData.ts | 29 ------- src/types/vulnerability.ts | 1 - src/watcher/manifestWatcher.ts | 30 ++++--- 22 files changed, 439 insertions(+), 196 deletions(-) create mode 100644 src/constants/policyRulesEnum.ts create mode 100644 src/constants/regex.ts create mode 100644 src/providers/dependencyPolicyProvider.ts create mode 100644 src/types/package.ts delete mode 100644 src/types/scannedData.ts diff --git a/src/constants/debricked_cli.ts b/src/constants/debricked_cli.ts index 3f136d2..a96348e 100644 --- a/src/constants/debricked_cli.ts +++ b/src/constants/debricked_cli.ts @@ -1,3 +1,4 @@ +import path from "path"; import { DebrickedCommandNode, Flag } from "../types"; import { Organization } from "./organization"; @@ -241,7 +242,7 @@ export class DebrickedCommands { label: "JSON Path", flag: "-j", description: "write upload result as json to provided path", - report: `${Organization.debrickedFolder}/${Organization.reports}/scan-output.json`, + report: path.join(Organization.reportsFolderPath, "scan-output.json"), }, { label: "Author", flag: "-a", description: "commit author" }, { label: "Branch", flag: "-b", description: "branch name", flagValue: "ide-PLACEHOLDER" }, diff --git a/src/constants/index.ts b/src/constants/index.ts index 16f8f66..43a81e2 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -3,5 +3,16 @@ import { Messages } from "./messages"; import { MessageStatus } from "./messageStatus"; import { Organization } from "./organization"; import { SecondService } from "./secondService"; +import { Regex } from "./regex"; +import { PolicyRules, PolicyTriggerEvents } from "./policyRulesEnum"; -export { DebrickedCommands, Organization, Messages, MessageStatus, SecondService }; +export { + DebrickedCommands, + Organization, + Messages, + MessageStatus, + SecondService, + Regex, + PolicyRules, + PolicyTriggerEvents, +}; diff --git a/src/constants/organization.ts b/src/constants/organization.ts index 2a069e6..3c76522 100644 --- a/src/constants/organization.ts +++ b/src/constants/organization.ts @@ -31,6 +31,7 @@ export class Organization { Organization.debrickedFolder, Organization.reports, ); + static readonly debrickedDataFile = "debricked_data.json"; static readonly debrickedDataFilePath = path.join( Organization.debrickedRootDir, diff --git a/src/constants/policyRulesEnum.ts b/src/constants/policyRulesEnum.ts new file mode 100644 index 0000000..f3ff393 --- /dev/null +++ b/src/constants/policyRulesEnum.ts @@ -0,0 +1,13 @@ +export enum PolicyRules { + warnPipeline = "Pipeline warning", + failPipeline = "Pipeline failing", + markUnaffected = "Mark vulnerability as unaffected", + markVulnerable = "Flag vulnerability as vulnerable", + sendEmail = "Notified email", + triggerWebhook = "Triggered webhook", +} + +export enum PolicyTriggerEvents { + WARN_PIPELINE = "warnPipeline", + FAIL_PIPELINE = "failPipeline", +} diff --git a/src/constants/regex.ts b/src/constants/regex.ts new file mode 100644 index 0000000..72f736a --- /dev/null +++ b/src/constants/regex.ts @@ -0,0 +1,7 @@ +export class Regex { + static readonly repoId = /\/repository\/(\d+)\//; + static readonly commitId = /\/commit\/(\d+)/; + static readonly packageJson = /"([^"]+)":\s*"([^"]+)"/; + static readonly goMod = + /^(?:require\s+)?(\S+)\s+(v?\d+(?:\.\d+)*(?:-[\w\.-]+)?(?:\+[\w\.-]+)?)(?:\s+\/\/\s+indirect)?/; +} diff --git a/src/extension.ts b/src/extension.ts index e0a46f5..bb122c4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -37,6 +37,7 @@ export async function activate(context: vscode.ExtensionContext) { await DebrickedCommand.commands(context); await providers.registerHover(context); + await providers.registerDependencyPolicyProvider(context); const debCommandsProvider = new DebrickedCommandsTreeDataProvider(); vscode.window.registerTreeDataProvider(Organization.debrickedCommand, debCommandsProvider); diff --git a/src/helpers/commonHelper.ts b/src/helpers/commonHelper.ts index d9f473f..0f9549f 100644 --- a/src/helpers/commonHelper.ts +++ b/src/helpers/commonHelper.ts @@ -3,6 +3,8 @@ import * as crypto from "crypto"; import { Logger } from "./loggerHelper"; import { ShowInputBoxHelper } from "./showInputBoxHelper"; import { GlobalStore } from "./globalStore"; +import path from "path"; +import * as vscode from "vscode"; export class Common { constructor( @@ -72,4 +74,46 @@ export class Common { public static stringToArray(inputString: string, separator: string): string[] { return inputString.split(separator).map((item) => item.trim().replace(/^\* /, "")); } + + public async isCurrentDocManifestFile(document: vscode.TextDocument) { + const selectedRepoName = this.globalStore.getRepository(); + const manifestFiles = await this.globalStore.getGlobalStateInstance()?.getGlobalData(selectedRepoName) + .filesToScan; + let currentManifestFile = path.basename(document.fileName); + currentManifestFile = currentManifestFile.endsWith(".git") + ? currentManifestFile.slice(0, -4) + : currentManifestFile; + + // Check if the current file is a manifest file + const isManifestFile = manifestFiles.some( + (manifest: string) => path.basename(manifest) === currentManifestFile, + ); + + return { isManifestFile, currentManifestFile }; + } + + /** + * Extracts a value from a URL using a regular expression. + * + * @param url The URL to extract the value from. + * @param regex The regular expression to use for extraction. + * @returns The extracted value, or null if the regular expression does not match. + */ + public extractValueFromStringUsingRegex(str: string, regex: RegExp, groupIndex: number = 1): string | null { + // Check if the str is a non-empty string + if (typeof str !== "string" || str === "") { + throw new Error("Invalid string"); + } + + // Check if the regular expression is valid + if (!(regex instanceof RegExp)) { + throw new Error("Invalid regular expression"); + } + + // Apply the regular expression to the URL + const match = str.match(regex); + + // Return the first capturing group if the regular expression matches + return match && match[groupIndex] ? match[groupIndex] : null; + } } diff --git a/src/helpers/fileHelper.ts b/src/helpers/fileHelper.ts index 6f28cbe..8359c07 100644 --- a/src/helpers/fileHelper.ts +++ b/src/helpers/fileHelper.ts @@ -4,14 +4,11 @@ import * as path from "path"; import { MessageStatus, Organization } from "../constants/index"; import { Logger } from "./loggerHelper"; import { DebrickedDataHelper } from "./debrickedDataHelper"; -import { GlobalStore } from "./globalStore"; -import { ScannedData } from "types/scannedData"; export class FileHelper { constructor( private debrickedDataHelper: DebrickedDataHelper, private logger: typeof Logger, - private globalStore: GlobalStore, ) {} /** * Stores content in a specified file within the 'debricked-result' folder. @@ -41,23 +38,18 @@ export class FileHelper { await this.openTextDocument(filePath); } - public async setRepoID() { - const data: ScannedData = JSON.parse( - fs.readFileSync(`${Organization.reportsFolderPath}/scan-output.json`, { - encoding: "utf8", - flag: "r", - }), - ); - const repoIdMatch = data.detailsUrl.match(/\/repository\/(\d+)\//); - const repoId = repoIdMatch ? Number(repoIdMatch[1]) : null; - - const commitMatch = data.detailsUrl.match(/\/commit\/(\d+)/); - const commitId = commitMatch ? Number(commitMatch[1]) : null; - - repoId ? this.globalStore.setRepoId(repoId) : null; - commitId ? this.globalStore.setCommitId(commitId) : null; - this.globalStore.setScanData(data); - - this.logger.logInfo("Found the repoId and commitId"); + /** + * Reads a JSON file and returns its contents as a JSON object. + * + * @param filePath The path to the JSON file. + * @param options The options for reading the file. + * @returns The JSON object. + */ + public readFileSync(filePath: fs.PathOrFileDescriptor, options?: { encoding: "utf8"; flag: "r" }): string | Buffer { + try { + return fs.readFileSync(filePath, options); + } catch (error: any) { + throw new Error(`Failed to read JSON file at ${filePath}: ${error}`); + } } } diff --git a/src/helpers/globalStore.ts b/src/helpers/globalStore.ts index 9dc5514..5bfff88 100644 --- a/src/helpers/globalStore.ts +++ b/src/helpers/globalStore.ts @@ -1,8 +1,6 @@ -import { Dependency } from "types/dependency"; import { MessageStatus } from "../constants/index"; import { GlobalState } from "./globalState"; -import { ScannedData } from "types/scannedData"; -import { DependencyVulnerability } from "types/vulnerability"; +import { DependencyVulnerability, Package } from "../types"; export class GlobalStore { private static instance: GlobalStore; @@ -10,10 +8,10 @@ export class GlobalStore { private sequenceID: string | undefined; private globalStateInstance: GlobalState | undefined; private repositoryName: string = MessageStatus.UNKNOWN; - private repoData: any; private repoId!: number; private commitId!: number; - private scannedData!: ScannedData; + private packages!: Map; + private vulnerableData!: Map; private constructor() {} @@ -85,14 +83,6 @@ export class GlobalStore { return this.repositoryName; } - public setDependencyData(repoData: any) { - this.repoData = repoData; - } - - public getDependencyData(): Map { - return this.repoData; - } - public getRepoId() { return this.repoId; } @@ -109,12 +99,12 @@ export class GlobalStore { this.commitId = commitId; } - public setScanData(data: ScannedData) { - this.scannedData = data; + public setPackages(data: Map) { + this.packages = data; } - public getScanData(): ScannedData { - return this.scannedData; + public getPackages(): Map { + return this.packages; } public setVulnerableData(data: Map) { diff --git a/src/helpers/index.ts b/src/helpers/index.ts index fdb971e..9a1fbff 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -60,7 +60,7 @@ const terminal = new Terminal(authHelper, Logger); const apiClient = new ApiClient(authHelper, errorHandler, Logger); const apiHelper = new ApiHelper(apiClient, Logger); const installHelper = new InstallHelper(Logger, statusBarMessageHelper, commandHelper); -const fileHelper = new FileHelper(debrickedDataHelper, Logger, globalStore); +const fileHelper = new FileHelper(debrickedDataHelper, Logger); const indexHelper = new IndexHelper(debrickedDataHelper, commonHelper, gitHelper); const showQuickPickHelper = new ShowQuickPickHelper(); const template = new Template(); diff --git a/src/helpers/template.ts b/src/helpers/template.ts index 997c736..2e17c5c 100644 --- a/src/helpers/template.ts +++ b/src/helpers/template.ts @@ -1,22 +1,13 @@ -import { PolicyViolation } from "types/scannedData"; import { Organization } from "../constants"; -import { Vulnerabilities } from "types/vulnerability"; +import { Vulnerabilities, Package } from "../types"; import * as vscode from "vscode"; -import { SecondService } from "../constants"; +import { SecondService, PolicyRules } from "../constants"; export class Template { constructor() {} - private policyViolation = { - failPipeline: "Pipeline failing", - warnPipeline: "Pipeline warning", - markUnaffected: "Mark vulnerability as unaffected", - markVulnerable: "Flag vulnerability as vulnerable", - sendEmail: "Notified email", - triggerWebhook: "Triggered webhook", - }; - public licenseContent(license: string, contents: vscode.MarkdownString) { + contents.appendText(Organization.separator); contents.appendMarkdown(`License: **${license}**`); contents.appendText(Organization.separator); } @@ -76,24 +67,26 @@ export class Template { contents.appendText(Organization.separator); } - public policyViolationContent(policyViolationData: PolicyViolation[], contents: vscode.MarkdownString) { - if (policyViolationData.length === 0) { + public policyViolationContent(policyViolationData: Package, contents: vscode.MarkdownString) { + if (policyViolationData?.policyRules === undefined) { contents.appendMarkdown("No policy violations found.\n"); + contents.appendText(Organization.separator); return; } contents.appendMarkdown("Policy Violations\n\n"); - policyViolationData.forEach((violation: PolicyViolation, index: number) => { - contents.appendMarkdown(`Rule - ${index + 1}`); - contents.appendMarkdown("\n"); - violation.ruleActions.forEach((ruleAction: string, index: number) => { - contents.appendMarkdown( - ` ${index + 1}. **${this.policyViolation[ruleAction as keyof typeof this.policyViolation]}** - [View rule](${violation.ruleLink})`, - ); - contents.appendMarkdown("\n"); - }); - contents.appendMarkdown("\n"); + contents.appendMarkdown("\n"); + policyViolationData.policyRules?.forEach((rule, index) => { + if (index < 2) { + rule.ruleActions?.forEach((ruleAction: string, index: number) => { + contents.appendMarkdown( + ` ${index + 1}. **${PolicyRules[ruleAction as keyof typeof PolicyRules]}** - [View rule](${rule.ruleLink})`, + ); + }); + contents.appendMarkdown("\n\n"); + } }); + contents.appendText(Organization.separator); } } diff --git a/src/providers/dependencyPolicyProvider.ts b/src/providers/dependencyPolicyProvider.ts new file mode 100644 index 0000000..49a4974 --- /dev/null +++ b/src/providers/dependencyPolicyProvider.ts @@ -0,0 +1,84 @@ +import { Package } from "types"; +import { commonHelper, globalStore } from "../helpers"; +import * as vscode from "vscode"; +import { PolicyTriggerEvents, SecondService } from "../constants"; + +export class DependencyPolicyProvider implements vscode.CodeActionProvider { + constructor(private diagnosticCollection: vscode.DiagnosticCollection) {} + + provideCodeActions(): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + return []; + } + + async checkPolicyViolation(document: vscode.TextDocument) { + // Check if the current file is a manifest file + const { isManifestFile, currentManifestFile } = await commonHelper.isCurrentDocManifestFile(document); + + if (!isManifestFile) { + return; + } + + if (currentManifestFile === "package.json") { + const diagnostics: vscode.Diagnostic[] = []; + const content = document.getText(); + const packages: Map = globalStore.getPackages(); + + if (packages && packages.size > 0) { + const manifestData = JSON.parse(content) || {}; + const allDependencies = { + ...manifestData.dependencies, + ...manifestData.devDependencies, + }; + + for (const [packageName, packageData] of packages) { + if (packageName in allDependencies) { + const range = this.findDependencyRange(document, packageName); + if (range) { + let diagnostic: vscode.Diagnostic | undefined; + packageData.policyRules?.forEach((rule) => { + if (rule.ruleActions?.includes(PolicyTriggerEvents.FAIL_PIPELINE)) { + diagnostic = new vscode.Diagnostic( + range, + `Dependency ${packageName} failed the pipeline`, + vscode.DiagnosticSeverity.Error, + ); + } else if (rule.ruleActions?.includes(PolicyTriggerEvents.WARN_PIPELINE)) { + diagnostic = new vscode.Diagnostic( + range, + `Dependency ${packageName} triggered a pipeline warning`, + vscode.DiagnosticSeverity.Warning, + ); + } + }); + + if (diagnostic) { + diagnostic.code = { + value: packageData.cve ?? "Unknown reason", + target: vscode.Uri.parse(packageData.cveLink ?? SecondService.debrickedBaseUrl), + }; + diagnostics.push(diagnostic); + } + } + } + } + } + + const uri = document.uri; + if (!uri.path.endsWith(".git")) { + this.diagnosticCollection.set(uri, diagnostics); + } + } + } + + private findDependencyRange(document: vscode.TextDocument, dependency: string): vscode.Range | null { + const text = document.getText(); + const dependencyPattern = new RegExp(`"${dependency}"\\s*:\\s*"[^"]*"`, "g"); + const match = dependencyPattern.exec(text); + if (match) { + const startPos = document.positionAt(match.index); + const endPos = document.positionAt(match.index + match[0].length); + return new vscode.Range(startPos, endPos); + } + return null; + } +} diff --git a/src/providers/index.ts b/src/providers/index.ts index 6d1e6a3..945a07a 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -3,13 +3,14 @@ import { ManifestDependencyHoverProvider } from "./manifestDependencyHoverProvid import * as vscode from "vscode"; import { MessageStatus } from "../constants/index"; import { Logger, errorHandler, gitHelper } from "../helpers"; +import { DependencyPolicyProvider } from "./dependencyPolicyProvider"; class Providers { public async registerHover(context: vscode.ExtensionContext) { try { Logger.logInfo("Started registering commands"); const selectedRepoName = await gitHelper.getUpstream(); - + if (selectedRepoName !== MessageStatus.UNKNOWN) { // Register hover provider context.subscriptions.push( @@ -25,7 +26,41 @@ class Providers { Logger.logInfo("Command registration has been completed"); } } -} + public async registerDependencyPolicyProvider(context: vscode.ExtensionContext) { + try { + Logger.logInfo("Started registering policy provider"); + const selectedRepoName = await gitHelper.getUpstream(); + + if (selectedRepoName !== MessageStatus.UNKNOWN) { + const diagnosticCollection = vscode.languages.createDiagnosticCollection("dependencyPolicyChecker"); + context.subscriptions.push(diagnosticCollection); + + const provider = new DependencyPolicyProvider(diagnosticCollection); + + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider({ scheme: "file" }, provider, { + providedCodeActionKinds: [vscode.CodeActionKind.QuickFix], + }), + ); + + // Trigger the check when a file is opened or saved + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument((doc) => provider.checkPolicyViolation(doc)), + vscode.workspace.onDidSaveTextDocument((doc) => provider.checkPolicyViolation(doc)), + vscode.window.onDidChangeActiveTextEditor((editor) => { + if (editor) { + provider.checkPolicyViolation(editor.document); + } + }), + ); + } + } catch (error) { + errorHandler.handleError(error); + } finally { + Logger.logInfo("Command registration has been completed"); + } + } +} const providers = new Providers(); export { DebrickedCommandsTreeDataProvider, ManifestDependencyHoverProvider, providers }; diff --git a/src/providers/manifestDependencyHoverProvider.ts b/src/providers/manifestDependencyHoverProvider.ts index 03f036f..fe3a90c 100644 --- a/src/providers/manifestDependencyHoverProvider.ts +++ b/src/providers/manifestDependencyHoverProvider.ts @@ -1,25 +1,14 @@ import * as vscode from "vscode"; -import * as path from "path"; -import { globalStore, template } from "../helpers"; -import { DependencyService } from "services"; -import { TransitiveVulnerabilities, Vulnerabilities } from "types/vulnerability"; -import { Dependency } from "types/dependency"; +import { commonHelper, globalStore, template } from "../helpers"; +import { Vulnerabilities, Package } from "../types"; +import { Regex } from "../constants"; export class ManifestDependencyHoverProvider implements vscode.HoverProvider { - private manifestFiles: string[] = []; - public async provideHover( document: vscode.TextDocument, position: vscode.Position, ): Promise { - const selectedRepoName = globalStore.getRepository(); - this.manifestFiles = await globalStore.getGlobalStateInstance()?.getGlobalData(selectedRepoName).filesToScan; - const currentManifestFile = path.basename(document.fileName); - - // Check if the current file is a manifest file - const isManifestFile = this.manifestFiles.some( - (manifest: string) => path.basename(manifest) === currentManifestFile, - ); + const { isManifestFile, currentManifestFile } = await commonHelper.isCurrentDocManifestFile(document); if (!isManifestFile) { return null; @@ -37,50 +26,51 @@ export class ManifestDependencyHoverProvider implements vscode.HoverProvider { return null; } - const depData = globalStore.getDependencyData().get(dependencyName); - const licenseData = depData?.licenses[0]?.name ?? "Unknown"; - const vulnerableData = await this.getVulnerableData(depData); - const policyViolationData = DependencyService.getPolicyViolationData(dependencyName); + const foundPackage = globalStore.getPackages().get(dependencyName); + + const licenseData = foundPackage?.licenses ? foundPackage?.licenses[0] : "Unknown"; + const vulnerableData = await this.getVulnerableData(foundPackage); const contents = this.createMarkdownString(); template.licenseContent(licenseData, contents); template.vulnerableContent(vulnerableData, contents); - template.policyViolationContent(policyViolationData, contents); + if (foundPackage) { + template.policyViolationContent(foundPackage, contents); + } return new vscode.Hover(contents); } - private async getVulnerableData(dependency?: Dependency): Promise { + private async getVulnerableData(dependency?: Package): Promise { const vulnerabilities: Vulnerabilities = { directVulnerabilities: [], indirectVulnerabilities: [], }; - const vulnerabilityData = globalStore.getVulnerableData(); - //direct dependencies - if (dependency) { - vulnerabilities.directVulnerabilities = vulnerabilityData.get(dependency.name.name) ?? []; - } - //indirect dependencies - if (dependency?.indirectDependencies) { - const vulnerabilitiesToFetch = dependency.indirectDependencies; - - for (const indirectDep of vulnerabilitiesToFetch) { - const vulnerableData = vulnerabilityData.get(indirectDep.name.name) ?? []; - - if (vulnerableData.length !== 0) { - const transitiveVulnerableData: TransitiveVulnerabilities = { - transitiveVulnerabilities: vulnerableData, - dependencyName: indirectDep.name.name, - dependencyId: indirectDep.id, - }; - vulnerabilities.indirectVulnerabilities.push(transitiveVulnerableData); - } + const vulnerabilityData = await globalStore.getVulnerableData(); - if (vulnerabilities.indirectVulnerabilities.length > 1) { - break; + if (dependency) { + // Direct dependencies + vulnerabilities.directVulnerabilities = vulnerabilityData.get(dependency.dependencyName) ?? []; + + // Indirect dependencies + if (dependency.indirectDependency) { + for (const [dependencyName, indirectDep] of dependency.indirectDependency) { + const vulnerableData = vulnerabilityData.get(dependencyName) ?? []; + + if (vulnerableData.length > 0) { + vulnerabilities.indirectVulnerabilities.push({ + transitiveVulnerabilities: vulnerableData, + dependencyName: indirectDep.dependencyName, + }); + } + + if (vulnerabilities.indirectVulnerabilities.length > 1) { + break; + } } } } + return vulnerabilities; } @@ -97,20 +87,17 @@ export class ManifestDependencyHoverProvider implements vscode.HoverProvider { switch (fileName) { case "package.json": { - const packageJsonRegex = /"([^"]+)":\s*"([^"]+)"/; - const match = lineText.match(packageJsonRegex); + const match = commonHelper.extractValueFromStringUsingRegex(lineText, Regex.packageJson); if (match) { - return match[1] + " (npm)"; + return match; } break; } case "go.mod": { - const goModRegex = - /^(?:require\s+)?(\S+)\s+(v?\d+(?:\.\d+)*(?:-[\w\.-]+)?(?:\+[\w\.-]+)?)(?:\s+\/\/\s+indirect)?/; - const match = lineText.match(goModRegex); + const match = commonHelper.extractValueFromStringUsingRegex(lineText, Regex.goMod); if (match) { - return match[1] + " (Go)"; + return match + " (Go)"; } break; } diff --git a/src/services/dependencyService.ts b/src/services/dependencyService.ts index 7cc1a87..24d18e8 100644 --- a/src/services/dependencyService.ts +++ b/src/services/dependencyService.ts @@ -1,7 +1,12 @@ -import { Dependency, DependencyResponse, IndirectDependency } from "types/dependency"; import { apiHelper, globalStore, Logger } from "../helpers"; -import { RequestParam } from "../types"; -import { DependencyVulnerability, DependencyVulnerabilityWrapper } from "types/vulnerability"; +import { Package, RequestParam } from "../types"; +import { + DependencyVulnerability, + DependencyVulnerabilityWrapper, + Dependency, + DependencyResponse, + IndirectDependency, +} from "../types"; import { SecondService } from "../constants"; export class DependencyService { @@ -13,19 +18,42 @@ export class DependencyService { commitId: commitId, }; const response: DependencyResponse = await apiHelper.get(requestParam); - const dependencyMap = new Map(); + const packageData = globalStore.getPackages(); - // Converts the response to map - response.dependencies.forEach((dep: Dependency) => { - dependencyMap.set(dep.name.name, dep); - if (dep.indirectDependencies.length > 0) { - dep.indirectDependencies.forEach((indirectDep: IndirectDependency) => { - dependencyMap.set(indirectDep.name.name, indirectDep); + response.dependencies.forEach((dependency: Dependency) => { + const depName = dependency.name.name?.split(" ")[0]; + const foundPackage = packageData.get(depName); + const newDependency: Package = { + licenses: [], + dependencyName: "", + }; + + if (foundPackage) { + const licenses = dependency.licenses.map((license) => license.name); + foundPackage.licenses = licenses; + Object.assign(newDependency, foundPackage); + } else { + newDependency.licenses = dependency.licenses.map((license) => license.name); + newDependency.dependencyName = depName; + } + + if (dependency.indirectDependencies.length > 0) { + const indirectDepsMap = new Map(); + + dependency.indirectDependencies.forEach((indirectDep: IndirectDependency) => { + const indirectDepName = indirectDep.name.name?.split(" ")[0]; + const newIndirectDep: Package = { + licenses: indirectDep.licenses.map((license) => license.name), + dependencyName: indirectDepName, + }; + indirectDepsMap.set(indirectDepName, newIndirectDep); }); + newDependency.indirectDependency = indirectDepsMap; } - }); - globalStore.setDependencyData(dependencyMap); + packageData.set(depName, newDependency); + }); + globalStore.setPackages(packageData); } static async getVulnerableData() { @@ -43,7 +71,7 @@ export class DependencyService { response.vulnerabilities.forEach((vul: DependencyVulnerability) => { vul.dependencies.forEach((dep) => { - const name = dep.name; + const name = dep.name.split(" ")[0]; if (!vulnerabilityMap.has(name)) { vulnerabilityMap.set(name, []); } @@ -53,19 +81,4 @@ export class DependencyService { globalStore.setVulnerableData(vulnerabilityMap); } - - static getPolicyViolationData(depName: string) { - Logger.logInfo("Started fetching Policy violation data"); - - const scannedData = globalStore.getScanData(); - - return scannedData.automationRules - .filter((automationRule) => - automationRule.triggerEvents.some((triggerEvent) => triggerEvent.dependency === depName), - ) - .map((automationRule) => ({ - ruleActions: automationRule.ruleActions, - ruleLink: automationRule.ruleLink, - })); - } } diff --git a/src/services/fileService.ts b/src/services/fileService.ts index 0a534f8..7318a79 100644 --- a/src/services/fileService.ts +++ b/src/services/fileService.ts @@ -6,9 +6,10 @@ import { errorHandler, globalStore, commonHelper, + fileHelper, } from "../helpers"; -import { DebrickedCommands, Messages, MessageStatus, Organization } from "../constants/index"; -import { DebrickedCommandNode } from "../types"; +import { DebrickedCommands, Messages, MessageStatus, Organization, Regex } from "../constants/index"; +import { DebrickedCommandNode, Package } from "../types"; import * as vscode from "vscode"; export class FileService { @@ -119,4 +120,58 @@ export class FileService { return debrickedData["unknown"].filesToScan; } } + + static async setRepoScannedData() { + const scannedFilePath = DebrickedCommands.SCAN.flags ? DebrickedCommands.SCAN.flags[2].report : ""; + let data; + if (scannedFilePath) { + data = JSON.parse(fileHelper.readFileSync(scannedFilePath).toString()); + } + const url = data.detailsUrl; + + const repoId = Number(commonHelper.extractValueFromStringUsingRegex(url, Regex.repoId)); + const commitId = Number(commonHelper.extractValueFromStringUsingRegex(url, Regex.commitId)); + + repoId ? globalStore.setRepoId(repoId) : null; + commitId ? globalStore.setCommitId(commitId) : null; + + FileService.processPackages(data.automationRules); + Logger.logInfo("Found the repoId and commitId"); + } + + static processPackages(automationRules: any[]) { + const actions = ["warnPipeline", "failPipeline"]; + + const triggerEventsMap = automationRules + .filter((rule) => actions.some((action) => rule.ruleActions.includes(action))) + .flatMap((rule) => + rule.triggerEvents.map((event: any) => { + const { dependency, ...restOfEvent } = event; + const { ruleActions, ruleLink } = rule; + return { + ...restOfEvent, + dependencyName: dependency.split(" ")[0], + policyRules: [{ ruleActions, ruleLink }], + }; + }), + ) + .reduce((map, event) => { + // for storing the multiple rules + const existingPackage = map.get(event.dependencyName); + if (existingPackage) { + const existingPolicyRules = existingPackage.policyRules || []; + const newPolicyRule = event.policyRules[0]; + + if (!existingPolicyRules.some((rule: any) => rule.ruleLink === newPolicyRule.ruleLink)) { + existingPackage.policyRules = [...existingPolicyRules, newPolicyRule]; + } + } else { + map.set(event.dependencyName, event); + } + + return map; + }, new Map()); + + globalStore.setPackages(triggerEventsMap); + } } diff --git a/src/services/scanService.ts b/src/services/scanService.ts index 88b75c6..63a6c90 100644 --- a/src/services/scanService.ts +++ b/src/services/scanService.ts @@ -8,13 +8,13 @@ import { commonHelper, commandHelper, authHelper, - fileHelper, } from "../helpers"; import { DebrickedCommands, MessageStatus, Organization, SecondService } from "../constants/index"; import { DebrickedCommandNode, Flag, RepositoryInfo } from "../types"; import * as vscode from "vscode"; import * as fs from "fs"; import { DependencyService } from "./dependencyService"; +import { FileService } from "./fileService"; export class ScanService { static async scanService() { @@ -39,7 +39,6 @@ export class ScanService { await ScanService.handleFlags(command.flags[2], cmdParams, currentRepoData); await ScanService.handleFlags(command.flags[3], cmdParams, currentRepoData); await ScanService.handleFlags(command.flags[4], cmdParams, currentRepoData); - await ScanService.handleFlags(command.global_flags[0], cmdParams, currentRepoData); } } else { Logger.logMessageByStatus(MessageStatus.WARN, `No default repo selected`); @@ -50,7 +49,6 @@ export class ScanService { await ScanService.handleFlags(command.flags[3], cmdParams, currentRepoData); await ScanService.handleFlags(command.flags[4], cmdParams, currentRepoData); await ScanService.handleFlags(command.flags[5], cmdParams, currentRepoData); - await ScanService.handleFlags(command.global_flags[0], cmdParams, currentRepoData); } } @@ -65,10 +63,15 @@ export class ScanService { progress.report({ message: "Scanning Manifest Files🚀" }); const output = await commandHelper.executeAsyncCommand( `${Organization.debrickedCli} ${cmdParams.join(" ")}`, + true, ); if (!output.includes(SecondService.repositoryBaseUrl)) { - if (await fs.existsSync(`${Organization.reportsFolderPath}/scan-output.json`)) { - await fileHelper.setRepoID(); + if ( + DebrickedCommands.SCAN.flags && + DebrickedCommands.SCAN.flags[2].report && + fs.existsSync(DebrickedCommands.SCAN.flags[2].report) + ) { + await FileService.setRepoScannedData(); const repoId = await globalStore.getRepoId(); const commitId = await globalStore.getCommitId(); diff --git a/src/types/index.ts b/src/types/index.ts index 380729e..24c2117 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,5 +3,27 @@ import { Flag } from "./flag"; import { InputBoxOptions } from "./inputBoxOptions"; import { RepositoryInfo } from "./repositoryInfo"; import { RequestParam } from "./requestParam"; +import { + DependencyVulnerabilityWrapper, + Vulnerabilities, + DependencyVulnerability, + TransitiveVulnerabilities, +} from "./vulnerability"; +import { Dependency, DependencyResponse, IndirectDependency } from "./dependency"; +import { Package } from "./package"; -export { DebrickedCommandNode, Flag, InputBoxOptions, RepositoryInfo, RequestParam }; +export { + DebrickedCommandNode, + Flag, + InputBoxOptions, + RepositoryInfo, + RequestParam, + DependencyVulnerabilityWrapper, + Vulnerabilities, + DependencyVulnerability, + TransitiveVulnerabilities, + Dependency, + DependencyResponse, + IndirectDependency, + Package, +}; diff --git a/src/types/package.ts b/src/types/package.ts new file mode 100644 index 0000000..65060c7 --- /dev/null +++ b/src/types/package.ts @@ -0,0 +1,17 @@ +export interface Package { + version?: string; + dependencyName: string; + dependencyLink?: string; + licenses?: string[]; + cve?: string; + cvss2?: string; + cvss3?: string; + cveLink?: string; + policyRules?: PolicyRules[]; + indirectDependency?: Map; +} + +interface PolicyRules { + ruleActions?: string[]; + ruleLink?: string; +} diff --git a/src/types/scannedData.ts b/src/types/scannedData.ts deleted file mode 100644 index 46b9677..0000000 --- a/src/types/scannedData.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface ScannedData { - vulnerabilitiesFound: number; - automationsAction: string; - automationRules: AutomationRules[]; - detailsUrl: string; -} - -interface AutomationRules { - ruleDescription: string; - ruleActions: string[]; - ruleLink: string; - triggered: boolean; - triggerEvents: TriggerEvents[]; -} - -interface TriggerEvents { - dependency: string; - dependencyLink: string; - licenses: string[]; - cve: string; - cvss2: number; - cvss3: number; - cveLink: string; -} - -export interface PolicyViolation { - ruleLink: string; - ruleActions: string[]; -} diff --git a/src/types/vulnerability.ts b/src/types/vulnerability.ts index cc74924..57a2273 100644 --- a/src/types/vulnerability.ts +++ b/src/types/vulnerability.ts @@ -33,5 +33,4 @@ export interface Vulnerabilities { export interface TransitiveVulnerabilities { transitiveVulnerabilities: DependencyVulnerability[]; dependencyName: string; - dependencyId: number; } diff --git a/src/watcher/manifestWatcher.ts b/src/watcher/manifestWatcher.ts index d37ef04..9fbe3ad 100644 --- a/src/watcher/manifestWatcher.ts +++ b/src/watcher/manifestWatcher.ts @@ -1,8 +1,8 @@ import * as vscode from "vscode"; import * as path from "path"; -import { MessageStatus, DebrickedCommands, Organization } from "../constants"; +import { MessageStatus, DebrickedCommands } from "../constants"; import { ScanService, FileService, DependencyService } from "../services"; -import { errorHandler, Logger, StatusMessage, statusBarMessageHelper, fileHelper, globalStore } from "../helpers"; +import { errorHandler, Logger, StatusMessage, statusBarMessageHelper, globalStore } from "../helpers"; export class ManifestWatcher { private static instance: ManifestWatcher; @@ -96,16 +96,20 @@ export class ManifestWatcher { } private async reportsWatcher(context: vscode.ExtensionContext) { - const watcher = vscode.workspace.createFileSystemWatcher(`${Organization.reportsFolderPath}/scan-output.json`); - watcher.onDidChange(async () => { - await fileHelper.setRepoID(); - - const repoId = await globalStore.getRepoId(); - const commitId = await globalStore.getCommitId(); - - await DependencyService.getDependencyData(repoId, commitId); - await DependencyService.getVulnerableData(); - }); - context.subscriptions.push(watcher); + const scannedFilePath = DebrickedCommands.SCAN.flags ? DebrickedCommands.SCAN.flags[2].report : ""; + let watcher; + if (scannedFilePath) { + watcher = vscode.workspace.createFileSystemWatcher(scannedFilePath); + watcher.onDidChange(async () => { + await FileService.setRepoScannedData(); + + const repoId = await globalStore.getRepoId(); + const commitId = await globalStore.getCommitId(); + + await DependencyService.getDependencyData(repoId, commitId); + await DependencyService.getVulnerableData(); + }); + context.subscriptions.push(watcher); + } } }