Skip to content

Commit

Permalink
Yarn - Fix incorrect impact graph
Browse files Browse the repository at this point in the history
  • Loading branch information
Or-Geva committed Sep 21, 2023
1 parent 13b96d1 commit eaea074
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { PackageType } from '../../../types/projectType';
import { DependenciesTreeNode } from '../dependenciesTreeNode';
import { DependencyScanResults } from '../../../types/workspaceIssuesDetails';
import { Utils } from '../../../utils/utils';
import { IImpactGraph, IImpactGraphNode } from 'jfrog-ide-webview';

export enum BuildTreeErrorType {
NotInstalled = '[Not Installed]',
NotSupported = '[Not Supported]'
}

export class RootNode extends DependenciesTreeNode {
public static IMPACT_PATHS_LIMIT: number = 50;
private _projectDetails: ProjectDetails;
private _workspaceFolder: string;

Expand Down Expand Up @@ -73,4 +75,50 @@ export class RootNode extends DependenciesTreeNode {
}
return result;
}

/**
* Retrieves the impact paths of all child components, recursively from a given root,
* The number of impact paths collected may be limited by the '{@link RootNode.IMPACT_PATHS_LIMIT}'.
* @param root - the root to get it's children impact
* @param componentWithIssue - the component to generate the impact path for it
* @param size - the total size of the impacted path
*/
public createImpactedGraph(vulnerableDependencyname: string, vulnerableDependencyversion: string): IImpactGraph {
return RootNode.collectPaths(vulnerableDependencyname + ':' + vulnerableDependencyversion, this.children, 0);
}

private static collectPaths(vulnerableDependencyId: string, children: DependenciesTreeNode[], size: number) {
let impactPaths: IImpactGraphNode[] = [];
for (let child of children) {
if (impactPaths.find(node => node.name === child.componentId)) {
// Loop encountered
continue;
}

if (child.componentId === vulnerableDependencyId) {
if (size < RootNode.IMPACT_PATHS_LIMIT) {
RootNode.appendDirectImpact(impactPaths, child.componentId);
}
size++;
}

let indirectImpact: IImpactGraph = this.collectPaths(vulnerableDependencyId, child.children, size);
RootNode.appendIndirectImpact(impactPaths, child.componentId, indirectImpact);
size = indirectImpact.pathsCount ?? size;
}
return { root: { children: impactPaths }, pathsCount: size } as IImpactGraph;
}

private static appendDirectImpact(impactPaths: IImpactGraphNode[], componentId: string): void {
impactPaths.push({ name: componentId } as IImpactGraphNode);
}

private static appendIndirectImpact(impactPaths: IImpactGraphNode[], componentId: string, indirectImpact: IImpactGraph): void {
if (!!indirectImpact.root.children?.length) {
impactPaths.push({
name: componentId,
children: indirectImpact.root.children
} as IImpactGraphNode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { DependenciesTreeNode } from '../dependenciesTreeNode';
import { BuildTreeErrorType, RootNode } from './rootTree';
import { PackageType } from '../../../types/projectType';
import { LogManager } from '../../../log/logManager';
import { IImpactGraph } from 'jfrog-ide-webview';
import { YarnImpactGraphUtil } from '../../utils/yarnImpactGraph';

export class YarnTreeNode extends RootNode {
private static readonly COMPONENT_PREFIX: string = 'npm://';
Expand All @@ -16,11 +18,11 @@ export class YarnTreeNode extends RootNode {
super(workspaceFolder, PackageType.Yarn, parent);
}

public refreshDependencies() {
let listResults: any;
public loadYarnDependencies() {
let results: any;
try {
listResults = this.runYarnList();
this.populateDependencyTree(this, listResults?.data?.trees);
results = this.runYarnList();
this.loadYarnList(this, results?.data?.trees);
} catch (error) {
this._logManager.logError(<any>error, false);
this._logManager.logMessageAndToastErr(
Expand All @@ -36,36 +38,48 @@ export class YarnTreeNode extends RootNode {
this.label = this.projectDetails.name;
}

private populateDependencyTree(dependencyTreeNode: DependenciesTreeNode, nodes: any[]) {
if (!nodes) {
/** @override */
public createImpactedGraph(name: string, version: string): IImpactGraph {
return new YarnImpactGraphUtil(name, version, this.generalInfo.getComponentId(), this.workspaceFolder).create();
}

/**
* Parse and load all yarn list's dependencies into concert object.
* @param parent - Parent Yarn dependency that is loaded into object from 'yarn list'.
* @param children - Child dependency of parent in Yarn list form
* @returns
*/
private loadYarnList(parent: DependenciesTreeNode, children: any[]) {
if (!children) {
return;
}
for (let node of nodes) {
for (let node of children) {
// Shadow dependencies does not always contain exact version, and therefore we should skip them.
if (node.shadow) {
continue;
}
const scope: string = NpmUtils.getDependencyScope(node.name);
let lastIndexOfAt: number = node.name.lastIndexOf('@');
let dependencyName: string = node.name.substring(0, lastIndexOfAt);
let dependencyVersion: string = node.name.substring(lastIndexOfAt + 1);

let generalInfo: GeneralInfo = new GeneralInfo(dependencyName, dependencyVersion, scope !== '' ? [scope] : [], '', PackageType.Yarn);
let hasRealChildren: boolean = this.hasRealChildren(node.children);
let treeCollapsibleState: vscode.TreeItemCollapsibleState = hasRealChildren
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None;
let componentId: string = dependencyName + ':' + dependencyVersion;
this.projectDetails.addDependency(YarnTreeNode.COMPONENT_PREFIX + componentId);

let child: DependenciesTreeNode = new DependenciesTreeNode(generalInfo, treeCollapsibleState, dependencyTreeNode);
child.dependencyId = YarnTreeNode.COMPONENT_PREFIX + componentId;
if (hasRealChildren) {
this.populateDependencyTree(child, node.children);
this.addDependency(parent, node);
if (this.hasRealChildren(node.children)) {
this.loadYarnList(parent, node.children);
}
}
}

private extractDependencyInfo(node: any): string[] {
const scope: string = NpmUtils.getDependencyScope(node.name);
let lastIndexOfAt: number = node.name.lastIndexOf('@');
let name: string = node.name.substring(0, lastIndexOfAt);
let version: string = node.name.substring(lastIndexOfAt + 1);
return [name, version, scope];
}

private addDependency(parent: DependenciesTreeNode, node: any): void {
const [dependencyName, dependencyVersion, scope] = this.extractDependencyInfo(node);
const generalInfo: GeneralInfo = new GeneralInfo(dependencyName, dependencyVersion, scope !== '' ? [scope] : [], '', PackageType.Yarn);
new DependenciesTreeNode(generalInfo, vscode.TreeItemCollapsibleState.None, parent).xrayId =
YarnTreeNode.COMPONENT_PREFIX + dependencyName + ':' + dependencyVersion;
}

/**
* Return true if the child dependencies contain any non shadowed dependency.
* @param childDependencies - Child dependencies at 'yarn list' results
Expand Down
47 changes: 6 additions & 41 deletions src/main/treeDataProviders/utils/dependencyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { MavenUtils } from '../../utils/mavenUtils';
import { NpmUtils } from '../../utils/npmUtils';
import { PypiUtils } from '../../utils/pypiUtils';
import { YarnUtils } from '../../utils/yarnUtils';
import { IImpactGraph, IImpactGraphNode, ILicense } from 'jfrog-ide-webview';
import { IImpactGraph, ILicense } from 'jfrog-ide-webview';
import { IssueTreeNode } from '../issuesTree/issueTreeNode';
import { FocusType } from '../../constants/contextKeys';
import { DependencyScanResults, ScanResults } from '../../types/workspaceIssuesDetails';
Expand All @@ -32,7 +32,6 @@ import { FileTreeNode } from '../issuesTree/fileTreeNode';

export class DependencyUtils {
public static readonly FAIL_TO_SCAN: string = '[Fail to scan]';
public static IMPACT_PATHS_LIMIT: number = 50;

/**
* Scan all the dependencies of a given package for security issues and populate the given data and view objects with the information.
Expand Down Expand Up @@ -328,17 +327,18 @@ export class DependencyUtils {
if (!issues) {
return paths;
}

for (let i: number = 0; i < issues.length; i++) {
let issue: IVulnerability = issues[i];
for (let [componentId, component] of Object.entries(issue.components)) {
const childGraph: IImpactGraph = this.getChildrenGraph(descriptorGraph, component, 0);
const ImpactedPaths: IImpactGraph = descriptorGraph.createImpactedGraph(component.package_name, component.package_version);
paths.set(issue.issue_id + componentId, {
root: {
name: this.getGraphName(descriptorGraph),
children: childGraph.root.children
children: ImpactedPaths.root.children
},
pathsCount: childGraph.pathsCount,
pathsLimit: DependencyUtils.IMPACT_PATHS_LIMIT
pathsCount: ImpactedPaths.pathsCount,
pathsLimit: RootNode.IMPACT_PATHS_LIMIT
} as IImpactGraph);
}
}
Expand All @@ -351,41 +351,6 @@ export class DependencyUtils {
: descriptorGraph.componentId;
}

/**
* Retrieves the impact paths of all child components, recursively from a given root,
* The number of impact paths collected may be limited by the '{@link DependencyUtils.IMPACT_PATHS_LIMIT}'.
* @param root - the root to get it's children impact
* @param componentWithIssue - the component to generate the impact path for it
* @param size - the total size of the impacted path
*/
private static getChildrenGraph(root: DependenciesTreeNode, componentWithIssue: IComponent, size: number): IImpactGraph {
let impactPaths: IImpactGraphNode[] = [];
for (let child of root.children) {
let impactChild: IImpactGraphNode | undefined = impactPaths.find(p => p.name === child.componentId);
if (!impactChild) {
if (child.componentId === componentWithIssue.package_name + ':' + componentWithIssue.package_version) {
// Direct impact
if (size < DependencyUtils.IMPACT_PATHS_LIMIT) {
impactPaths.push({
name: child.componentId
} as IImpactGraphNode);
}
size++;
}
// indirect impact
let indirectImpact: IImpactGraph = this.getChildrenGraph(child, componentWithIssue, size);
if (!!indirectImpact.root.children?.length) {
impactPaths.push({
name: child.componentId,
children: indirectImpact.root.children
} as IImpactGraphNode);
}
size = indirectImpact.pathsCount ?? size;
}
}
return { root: { children: impactPaths }, pathsCount: size } as IImpactGraph;
}

/**
* Populate the provided issues data to the project node (view element)
* @param projectNode - the project node that will be populated
Expand Down
Loading

0 comments on commit eaea074

Please sign in to comment.