Skip to content

Commit

Permalink
12 hover provider for manifest files (#46)
Browse files Browse the repository at this point in the history
* added policy violations per dependency

* added regex method

* refactored

* code refactor

* code refactor

* refactored and cleaned

* refactored

---------

Co-authored-by: Nagarjun Sanji <[email protected]>
  • Loading branch information
rajpreet-s and Nagarjun Sanji authored Aug 27, 2024
1 parent 09a0efe commit 6330445
Show file tree
Hide file tree
Showing 22 changed files with 439 additions and 196 deletions.
3 changes: 2 additions & 1 deletion src/constants/debricked_cli.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from "path";
import { DebrickedCommandNode, Flag } from "../types";
import { Organization } from "./organization";

Expand Down Expand Up @@ -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" },
Expand Down
13 changes: 12 additions & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
1 change: 1 addition & 0 deletions src/constants/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class Organization {
Organization.debrickedFolder,
Organization.reports,
);

static readonly debrickedDataFile = "debricked_data.json";
static readonly debrickedDataFilePath = path.join(
Organization.debrickedRootDir,
Expand Down
13 changes: 13 additions & 0 deletions src/constants/policyRulesEnum.ts
Original file line number Diff line number Diff line change
@@ -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",
}
7 changes: 7 additions & 0 deletions src/constants/regex.ts
Original file line number Diff line number Diff line change
@@ -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)?/;
}
1 change: 1 addition & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
44 changes: 44 additions & 0 deletions src/helpers/commonHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
}
}
34 changes: 13 additions & 21 deletions src/helpers/fileHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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}`);
}
}
}
24 changes: 7 additions & 17 deletions src/helpers/globalStore.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
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;
private userId: string | undefined;
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<string, Package>;

private vulnerableData!: Map<string, DependencyVulnerability[]>;

private constructor() {}
Expand Down Expand Up @@ -85,14 +83,6 @@ export class GlobalStore {
return this.repositoryName;
}

public setDependencyData(repoData: any) {
this.repoData = repoData;
}

public getDependencyData(): Map<string, Dependency> {
return this.repoData;
}

public getRepoId() {
return this.repoId;
}
Expand All @@ -109,12 +99,12 @@ export class GlobalStore {
this.commitId = commitId;
}

public setScanData(data: ScannedData) {
this.scannedData = data;
public setPackages(data: Map<string, Package>) {
this.packages = data;
}

public getScanData(): ScannedData {
return this.scannedData;
public getPackages(): Map<string, Package> {
return this.packages;
}

public setVulnerableData(data: Map<string, DependencyVulnerability[]>) {
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
41 changes: 17 additions & 24 deletions src/helpers/template.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down Expand Up @@ -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);
}
}
84 changes: 84 additions & 0 deletions src/providers/dependencyPolicyProvider.ts
Original file line number Diff line number Diff line change
@@ -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<string, Package> = 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;
}
}
Loading

0 comments on commit 6330445

Please sign in to comment.