From ce56e9d1e9f73a1da2e8b30abccf0a28e9cc3c68 Mon Sep 17 00:00:00 2001 From: Rajpreet Singh <63117988+rajpreet-s@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:26:55 +0530 Subject: [PATCH] added transitive vulnerability (#40) --- src/constants/index.ts | 3 +- src/constants/organization.ts | 9 +- src/constants/secondService.ts | 9 ++ src/helpers/apiHelper.ts | 4 +- src/helpers/fileHelper.ts | 4 +- src/helpers/globalStore.ts | 20 ++++ src/helpers/template.ts | 98 ++++++++++++++++--- .../manifestDependencyHoverProvider.ts | 48 +++++++-- src/services/dependencyService.ts | 41 ++++++-- src/services/scanService.ts | 5 +- src/types/scannedData.ts | 29 ++++++ src/types/vulnerability.ts | 21 +++- src/watcher/manifestWatcher.ts | 1 + 13 files changed, 247 insertions(+), 45 deletions(-) create mode 100644 src/constants/secondService.ts create mode 100644 src/types/scannedData.ts diff --git a/src/constants/index.ts b/src/constants/index.ts index 4006aba..16f8f66 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -2,5 +2,6 @@ import { DebrickedCommands } from "./debricked_cli"; import { Messages } from "./messages"; import { MessageStatus } from "./messageStatus"; import { Organization } from "./organization"; +import { SecondService } from "./secondService"; -export { DebrickedCommands, Organization, Messages, MessageStatus }; +export { DebrickedCommands, Organization, Messages, MessageStatus, SecondService }; diff --git a/src/constants/organization.ts b/src/constants/organization.ts index 8c04dd5..beb1e94 100644 --- a/src/constants/organization.ts +++ b/src/constants/organization.ts @@ -4,12 +4,6 @@ import * as os from "os"; export class Organization { static readonly name = "debricked"; - static readonly apiVersion = "1.0"; - static readonly debrickedBaseUrl = "https://debricked.com"; - static readonly baseUrl = `${Organization.debrickedBaseUrl}/api/${Organization.apiVersion}/`; - - static readonly dependencyUrl = "open/dependencies/get-dependencies-hierarchy"; - static readonly vulnerableUrl = "open/vulnerabilities/get-vulnerabilities"; static readonly nameCaps = "Debricked"; // Command and OS-specific constants @@ -59,6 +53,9 @@ export class Organization { static readonly bearerTokenKey = "bearerToken"; static readonly userId = "userId"; + // Markdown + static readonly separator = "\n______________________________\n"; + // Messages static readonly UNSUPPORTED_OS = "Unsupported operating system"; diff --git a/src/constants/secondService.ts b/src/constants/secondService.ts new file mode 100644 index 0000000..8452e30 --- /dev/null +++ b/src/constants/secondService.ts @@ -0,0 +1,9 @@ +export class SecondService { + static readonly apiVersion = "1.0"; + static readonly debrickedBaseUrl = "https://debricked.com"; + static readonly baseUrl = `${SecondService.debrickedBaseUrl}/api/${SecondService.apiVersion}/`; + + static readonly dependencyUrl = "open/dependencies/get-dependencies-hierarchy"; + static readonly vulnerableUrl = "open/vulnerabilities/get-vulnerabilities"; + static readonly repositoryBaseUrl = "https://debricked.com/app/en/repository/"; +} diff --git a/src/helpers/apiHelper.ts b/src/helpers/apiHelper.ts index 51d1515..21498f0 100644 --- a/src/helpers/apiHelper.ts +++ b/src/helpers/apiHelper.ts @@ -1,7 +1,7 @@ import { ApiClient } from "./apiClient"; import { RequestParam } from "../types"; import { Logger } from "./loggerHelper"; -import { Organization } from "../constants/index"; +import { SecondService } from "../constants"; export class ApiHelper { constructor( @@ -10,7 +10,7 @@ export class ApiHelper { ) {} public async get(requestParam: RequestParam): Promise { - let url = `${Organization.baseUrl}${requestParam.endpoint}`; + let url = `${SecondService.baseUrl}${requestParam.endpoint}`; const params = []; if (requestParam.page) { diff --git a/src/helpers/fileHelper.ts b/src/helpers/fileHelper.ts index 8960ca7..6f28cbe 100644 --- a/src/helpers/fileHelper.ts +++ b/src/helpers/fileHelper.ts @@ -5,6 +5,7 @@ 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( @@ -41,7 +42,7 @@ export class FileHelper { } public async setRepoID() { - const data = JSON.parse( + const data: ScannedData = JSON.parse( fs.readFileSync(`${Organization.reportsFolderPath}/scan-output.json`, { encoding: "utf8", flag: "r", @@ -55,6 +56,7 @@ export class FileHelper { repoId ? this.globalStore.setRepoId(repoId) : null; commitId ? this.globalStore.setCommitId(commitId) : null; + this.globalStore.setScanData(data); this.logger.logInfo("Found the repoId and commitId"); } diff --git a/src/helpers/globalStore.ts b/src/helpers/globalStore.ts index e6b80e8..9dc5514 100644 --- a/src/helpers/globalStore.ts +++ b/src/helpers/globalStore.ts @@ -1,6 +1,8 @@ import { Dependency } from "types/dependency"; import { MessageStatus } from "../constants/index"; import { GlobalState } from "./globalState"; +import { ScannedData } from "types/scannedData"; +import { DependencyVulnerability } from "types/vulnerability"; export class GlobalStore { private static instance: GlobalStore; @@ -11,6 +13,8 @@ export class GlobalStore { private repoData: any; private repoId!: number; private commitId!: number; + private scannedData!: ScannedData; + private vulnerableData!: Map; private constructor() {} @@ -104,4 +108,20 @@ export class GlobalStore { public setCommitId(commitId: number) { this.commitId = commitId; } + + public setScanData(data: ScannedData) { + this.scannedData = data; + } + + public getScanData(): ScannedData { + return this.scannedData; + } + + public setVulnerableData(data: Map) { + this.vulnerableData = data; + } + + public getVulnerableData(): Map { + return this.vulnerableData; + } } diff --git a/src/helpers/template.ts b/src/helpers/template.ts index 9402306..997c736 100644 --- a/src/helpers/template.ts +++ b/src/helpers/template.ts @@ -1,33 +1,99 @@ +import { PolicyViolation } from "types/scannedData"; import { Organization } from "../constants"; -import { DependencyVulnerability } from "types/vulnerability"; +import { Vulnerabilities } from "types/vulnerability"; import * as vscode from "vscode"; +import { SecondService } from "../constants"; export class Template { constructor() {} - public licenseContent(data: string, contents: vscode.MarkdownString) { - contents.appendMarkdown(`License: **${data}**`); - contents.appendText("\n______________________________\n"); + + 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.appendMarkdown(`License: **${license}**`); + contents.appendText(Organization.separator); } - public vulnerableContent(data: DependencyVulnerability[], contents: vscode.MarkdownString): void { - if (data.length === 0) { - contents.appendMarkdown("No vulnerabilities found"); - return; + public vulnerableContent(vulnerabilities: Vulnerabilities, contents: vscode.MarkdownString): void { + // direct vulnerabilities + if (vulnerabilities.directVulnerabilities.length === 0) { + contents.appendMarkdown("No vulnerabilities found\n\n"); + } else { + contents.appendMarkdown( + `Direct Vulnerabilities Found: **${vulnerabilities.directVulnerabilities.length}**\n\n`, + ); + + const vulnerabilitiesToShow = vulnerabilities.directVulnerabilities.slice(0, 2); + vulnerabilitiesToShow.forEach((vulnerability) => { + contents.appendMarkdown( + `[**${vulnerability.cveId}**](${SecondService.debrickedBaseUrl + vulnerability.name.link})`, + ); + + if (vulnerability.cvss) { + contents.appendMarkdown(` - CVSS: ${vulnerability.cvss.text} (${vulnerability.cvss.type})`); + } + + contents.appendMarkdown("\n\n"); + }); } - contents.appendMarkdown(`Vulnerabilities Found: **${data.length}**\n\n`); + contents.appendText(Organization.separator); - const vulnerabilitiesToShow = data.slice(0, 2); - vulnerabilitiesToShow.forEach((vulnerability) => { + // transitive vulnerabilities + if (vulnerabilities.indirectVulnerabilities.length === 0) { + contents.appendMarkdown("No transitive vulnerabilities found"); + } else { contents.appendMarkdown( - `[**${vulnerability.cveId}**](${Organization.debrickedBaseUrl + vulnerability.name.link})`, + `Transitive Vulnerabilities Found: **${vulnerabilities.indirectVulnerabilities.length}**`, ); + contents.appendMarkdown("\n\n"); + vulnerabilities.indirectVulnerabilities.forEach((indirectVulnerability) => { + const vulnerabilitiesToShow = indirectVulnerability; + contents.appendMarkdown(`${indirectVulnerability.dependencyName}`); + contents.appendMarkdown("\n\n"); - if (vulnerability.cvss) { - contents.appendMarkdown(` - CVSS: ${vulnerability.cvss.text} (${vulnerability.cvss.type})`); - } + vulnerabilitiesToShow.transitiveVulnerabilities.forEach((vulnerability) => { + contents.appendMarkdown( + `[**${vulnerability.cveId}**](${SecondService.debrickedBaseUrl + vulnerability.name.link})`, + ); - contents.appendMarkdown("\n\n"); + if (vulnerability.cvss) { + contents.appendMarkdown(` - CVSS: ${vulnerability.cvss.text} (${vulnerability.cvss.type})`); + } + + contents.appendMarkdown("\n\n"); + }); + }); + } + + contents.appendText(Organization.separator); + } + + public policyViolationContent(policyViolationData: PolicyViolation[], contents: vscode.MarkdownString) { + if (policyViolationData.length === 0) { + contents.appendMarkdown("No policy violations found.\n"); + 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"); }); } } diff --git a/src/providers/manifestDependencyHoverProvider.ts b/src/providers/manifestDependencyHoverProvider.ts index dda5fc7..03f036f 100644 --- a/src/providers/manifestDependencyHoverProvider.ts +++ b/src/providers/manifestDependencyHoverProvider.ts @@ -2,7 +2,8 @@ import * as vscode from "vscode"; import * as path from "path"; import { globalStore, template } from "../helpers"; import { DependencyService } from "services"; -import { DependencyVulnerability } from "types/vulnerability"; +import { TransitiveVulnerabilities, Vulnerabilities } from "types/vulnerability"; +import { Dependency } from "types/dependency"; export class ManifestDependencyHoverProvider implements vscode.HoverProvider { private manifestFiles: string[] = []; @@ -37,21 +38,50 @@ export class ManifestDependencyHoverProvider implements vscode.HoverProvider { } const depData = globalStore.getDependencyData().get(dependencyName); - const licenseData = depData?.licenses[0]?.name ?? "License information unavailable"; - const vulnerableData = await this.getVulnerableData(depData?.id); + const licenseData = depData?.licenses[0]?.name ?? "Unknown"; + const vulnerableData = await this.getVulnerableData(depData); + const policyViolationData = DependencyService.getPolicyViolationData(dependencyName); const contents = this.createMarkdownString(); template.licenseContent(licenseData, contents); template.vulnerableContent(vulnerableData, contents); + template.policyViolationContent(policyViolationData, contents); return new vscode.Hover(contents); } - private async getVulnerableData(dependencyId?: number): Promise { - if (dependencyId) { - return await DependencyService.getVulnerableData(dependencyId); + private async getVulnerableData(dependency?: Dependency): Promise { + const vulnerabilities: Vulnerabilities = { + directVulnerabilities: [], + indirectVulnerabilities: [], + }; + const vulnerabilityData = globalStore.getVulnerableData(); + //direct dependencies + if (dependency) { + vulnerabilities.directVulnerabilities = vulnerabilityData.get(dependency.name.name) ?? []; } - return []; + //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); + } + + if (vulnerabilities.indirectVulnerabilities.length > 1) { + break; + } + } + } + return vulnerabilities; } private createMarkdownString(): vscode.MarkdownString { @@ -82,7 +112,11 @@ export class ManifestDependencyHoverProvider implements vscode.HoverProvider { if (match) { return match[1] + " (Go)"; } + break; } + + default: + break; } return null; } diff --git a/src/services/dependencyService.ts b/src/services/dependencyService.ts index 39bdf0e..7cc1a87 100644 --- a/src/services/dependencyService.ts +++ b/src/services/dependencyService.ts @@ -1,14 +1,14 @@ import { Dependency, DependencyResponse, IndirectDependency } from "types/dependency"; import { apiHelper, globalStore, Logger } from "../helpers"; import { RequestParam } from "../types"; -import { DependencyVulnerabilityWrapper } from "types/vulnerability"; -import { Organization } from "../constants"; +import { DependencyVulnerability, DependencyVulnerabilityWrapper } from "types/vulnerability"; +import { SecondService } from "../constants"; export class DependencyService { static async getDependencyData(repoID: number, commitId: number) { Logger.logInfo("Started fetching the Dependency Data"); const requestParam: RequestParam = { - endpoint: Organization.dependencyUrl, + endpoint: SecondService.dependencyUrl, repoId: repoID, commitId: commitId, }; @@ -28,19 +28,44 @@ export class DependencyService { globalStore.setDependencyData(dependencyMap); } - static async getVulnerableData(depId: number) { + static async getVulnerableData() { Logger.logInfo("Started fetching the Vulnerable Data"); const repoId = await globalStore.getRepoId(); const commitId = await globalStore.getCommitId(); const requestParam: RequestParam = { - endpoint: Organization.vulnerableUrl, + endpoint: SecondService.vulnerableUrl, repoId: repoId, commitId: commitId, - dependencyId: depId, }; const response: DependencyVulnerabilityWrapper = await apiHelper.get(requestParam); - const vulnerableData = response.vulnerabilities; - return vulnerableData; + const vulnerabilityMap = new Map(); + + response.vulnerabilities.forEach((vul: DependencyVulnerability) => { + vul.dependencies.forEach((dep) => { + const name = dep.name; + if (!vulnerabilityMap.has(name)) { + vulnerabilityMap.set(name, []); + } + vulnerabilityMap.get(name)!.push(vul); + }); + }); + + 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/scanService.ts b/src/services/scanService.ts index 7242a3f..88b75c6 100644 --- a/src/services/scanService.ts +++ b/src/services/scanService.ts @@ -10,7 +10,7 @@ import { authHelper, fileHelper, } from "../helpers"; -import { DebrickedCommands, MessageStatus, Organization } from "../constants/index"; +import { DebrickedCommands, MessageStatus, Organization, SecondService } from "../constants/index"; import { DebrickedCommandNode, Flag, RepositoryInfo } from "../types"; import * as vscode from "vscode"; import * as fs from "fs"; @@ -66,7 +66,7 @@ export class ScanService { const output = await commandHelper.executeAsyncCommand( `${Organization.debrickedCli} ${cmdParams.join(" ")}`, ); - if (!output.includes("https://debricked.com/app/en/repository/")) { + if (!output.includes(SecondService.repositoryBaseUrl)) { if (await fs.existsSync(`${Organization.reportsFolderPath}/scan-output.json`)) { await fileHelper.setRepoID(); @@ -74,6 +74,7 @@ export class ScanService { const commitId = await globalStore.getCommitId(); await DependencyService.getDependencyData(repoId, commitId); + await DependencyService.getVulnerableData(); } else { throw new Error("No reports file exists"); } diff --git a/src/types/scannedData.ts b/src/types/scannedData.ts new file mode 100644 index 0000000..46b9677 --- /dev/null +++ b/src/types/scannedData.ts @@ -0,0 +1,29 @@ +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 394fa7e..cc74924 100644 --- a/src/types/vulnerability.ts +++ b/src/types/vulnerability.ts @@ -6,7 +6,8 @@ export interface DependencyVulnerability { cveId: string; cvss: CVSS; cpeVersions: string[]; - name: VulnerabilityName + name: VulnerabilityName; + dependencies: Dependencies[]; } interface CVSS { @@ -17,4 +18,20 @@ interface CVSS { interface VulnerabilityName { name: string; link: string; -} \ No newline at end of file +} + +interface Dependencies { + name: string; + shortName: string; + link: string; +} +export interface Vulnerabilities { + directVulnerabilities: DependencyVulnerability[]; + indirectVulnerabilities: TransitiveVulnerabilities[]; +} + +export interface TransitiveVulnerabilities { + transitiveVulnerabilities: DependencyVulnerability[]; + dependencyName: string; + dependencyId: number; +} diff --git a/src/watcher/manifestWatcher.ts b/src/watcher/manifestWatcher.ts index 79466e6..d37ef04 100644 --- a/src/watcher/manifestWatcher.ts +++ b/src/watcher/manifestWatcher.ts @@ -104,6 +104,7 @@ export class ManifestWatcher { const commitId = await globalStore.getCommitId(); await DependencyService.getDependencyData(repoId, commitId); + await DependencyService.getVulnerableData(); }); context.subscriptions.push(watcher); }