Skip to content

Commit

Permalink
ApplicabilityScan - Add indirect-cve to scanner (#450)
Browse files Browse the repository at this point in the history
Add support for applicability scanning of indirect (transitive) CVEs.
This is done by sending a separate list of detected indirect CVEs (indirect-cve-whitelist) to the applicability scanner YAML configuration file.
  • Loading branch information
srmish-jfrog authored Dec 13, 2023
1 parent 702011f commit 1a9ae97
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 26 deletions.
74 changes: 54 additions & 20 deletions src/main/scanLogic/scanRunners/applicabilityScan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,21 @@ export interface ApplicabilityScanArgs extends AnalyzeScanRequest {
grep_disable: boolean;
// Must have at least one item, the CVE to search for in scan
cve_whitelist: string[];
indirect_cve_whitelist: string[];
}

/**
* The response that is generated from the binary after scanning applicability
*/
export interface ApplicabilityScanResponse {
// All the cve that were scanned (have data about them in analyzer)
// All the direct cve that were scanned (have data about them in analyzer)
scannedCve: string[];
// All the indirect cves that should be scanned
indirectCve: string[];
// All the cve that have applicable issues
applicableCve: { [cve_id: string]: CveApplicableDetails };
// All the cve that have non-applicable issues
nonapplicableCve: string[];
}

/**
Expand All @@ -44,6 +49,9 @@ export interface CveApplicableDetails {
fullDescription?: string;
}

export class BundleCves extends Map<FileScanBundle, [Set<string>, Set<string>]> {}


/**
* Describes a runner for the Applicability scan.
*/
Expand All @@ -67,7 +75,7 @@ export class ApplicabilityRunner extends JasRunner {
/** @override */
public requestsToYaml(...requests: AnalyzeScanRequest[]): string {
let str: string = super.requestsToYaml(...requests);
return str.replace('cve_whitelist', 'cve-whitelist');
return str.replace('cve_whitelist', 'cve-whitelist').replace('indirect_cve_whitelist', 'indirect-cve-whitelist');
}

/** @override */
Expand All @@ -85,7 +93,7 @@ export class ApplicabilityRunner extends JasRunner {
/** @override */
protected logStartScanning(request: ApplicabilityScanArgs): void {
this._logManager.logMessage(
`Scanning directory ' ${request.roots[0]} + ', for ${this._scanType} issues: ${request.cve_whitelist} Skipping folders: ${request.skipped_folders}`,
`Scanning directory ' ${request.roots[0]} + ', for ${this._scanType} issues: ${request.cve_whitelist} indirect issues: ${request.indirect_cve_whitelist} Skipping folders: ${request.skipped_folders}`,
'DEBUG'
);
}
Expand All @@ -94,45 +102,62 @@ export class ApplicabilityRunner extends JasRunner {
* Scan for applicability issues
*/
public async scan(): Promise<void> {
let filteredBundles: Map<FileScanBundle, Set<string>> = this.filterBundlesWithoutIssuesToScan();
let workspaceToBundles: Map<string, Map<FileScanBundle, Set<string>>> = this.mapBundlesForApplicableScanning(filteredBundles);
let filteredBundles: BundleCves = this.filterBundlesWithoutIssuesToScan();
let workspaceToBundles: Map<string, BundleCves> = this.mapBundlesForApplicableScanning(filteredBundles);
if (workspaceToBundles.size == 0) {
return;
}
let excludePatterns: string[] = AnalyzerUtils.getAnalyzerManagerExcludePatterns(Configuration.getScanExcludePattern());
for (let [workspacePath, bundles] of workspaceToBundles) {
let cveToScan: Set<string> = Utils.combineSets(Array.from(bundles.values()));
// Unpack the direct & indirect CVEs
const directCveSets: Set<string>[] = [];
const indirectCveSets: Set<string>[] = [];
for (const cvesTuple of bundles.values()) {
directCveSets.push(cvesTuple[0]);
indirectCveSets.push(cvesTuple[1]);
}

const cveToScan: Set<string> = Utils.combineSets(directCveSets);
const indirectCveToScan: Set<string> = Utils.combineSets(indirectCveSets);
// Scan workspace for all cve in relevant bundles
let startApplicableTime: number = Date.now();

const request: ApplicabilityScanArgs = {
type: ScanType.AnalyzeApplicability,
roots: [workspacePath],
cve_whitelist: Array.from(cveToScan),
indirect_cve_whitelist: Array.from(indirectCveToScan),
skipped_folders: excludePatterns
} as ApplicabilityScanArgs;

// Merge the direct and indirect CVEs
const mergedBundles: Map<FileScanBundle, Set<string>> = new Map<FileScanBundle, Set<string>>();
for (const [fileScanBundle, cvesTuple] of bundles) {
mergedBundles.set(fileScanBundle, Utils.combineSets(cvesTuple));
}

this.logStartScanning(request);
let response: AnalyzerScanResponse | undefined = await this.executeRequest(this._progressManager.checkCancel, request);
let applicableIssues: ApplicabilityScanResponse = this.convertResponse(response);
if (applicableIssues?.applicableCve) {
this.transferApplicableResponseToBundles(applicableIssues, bundles, startApplicableTime);
this.transferApplicableResponseToBundles(applicableIssues, mergedBundles, startApplicableTime);
}
}
}

/**
* Filter bundles without direct cve issues, transform the bundle list to have its relevant cve to scan set.
* @returns Map of bundles to their set of direct cves issues, with at least one for each bundle
* Filter bundles without direct or indirect cves, transform the bundle list to have its relevant cve to scan set.
* @returns Map of bundles to their sets of direct and indirect cves, with at least one for each bundle
*/
private filterBundlesWithoutIssuesToScan(): Map<FileScanBundle, Set<string>> {
let filtered: Map<FileScanBundle, Set<string>> = new Map<FileScanBundle, Set<string>>();
private filterBundlesWithoutIssuesToScan(): BundleCves {
let filtered: BundleCves = new BundleCves();
for (let fileScanBundle of this._bundlesWithIssues) {
if (!(fileScanBundle.dataNode instanceof ProjectDependencyTreeNode)) {
// Filter non dependencies projects
continue;
}
let cvesToScan: Set<string> = new Set<string>();
const cvesToScan: Set<string> = new Set<string>();
const indirectCvesToScan: Set<string> = new Set<string>();
fileScanBundle.dataNode.issues.forEach((issue: IssueTreeNode) => {
if (!(issue instanceof CveTreeNode) || !issue.cve?.cve) {
return;
Expand All @@ -141,14 +166,16 @@ export class ApplicabilityRunner extends JasRunner {
// Other project types should include only CVEs on direct dependencies.
if (this._packageType === PackageType.Python || !issue.parent.indirect) {
cvesToScan.add(issue.cve.cve);
} else {
indirectCvesToScan.add(issue.cve.cve);
}
});
if (cvesToScan.size == 0) {
if (cvesToScan.size == 0 && indirectCvesToScan.size == 0) {
// Nothing to do in bundle
continue;
}

filtered.set(fileScanBundle, cvesToScan);
filtered.set(fileScanBundle, [cvesToScan, indirectCvesToScan]);
}

return filtered;
Expand All @@ -159,17 +186,19 @@ export class ApplicabilityRunner extends JasRunner {
* @param filteredBundles - bundles to map
* @returns mapped bundles to similar workspace
*/
private mapBundlesForApplicableScanning(filteredBundles: Map<FileScanBundle, Set<string>>): Map<string, Map<FileScanBundle, Set<string>>> {
let workspaceToScanBundles: Map<string, Map<FileScanBundle, Set<string>>> = new Map<string, Map<FileScanBundle, Set<string>>>();
private mapBundlesForApplicableScanning(
filteredBundles: BundleCves
): Map<string, BundleCves> {
let workspaceToScanBundles: Map<string, BundleCves> = new Map<string, BundleCves>();

for (let [fileScanBundle, cvesToScan] of filteredBundles) {
for (let [fileScanBundle, cvesTuple] of filteredBundles) {
let descriptorIssues: DependencyScanResults = <DependencyScanResults>fileScanBundle.data;
// Map information to similar directory space
let workspacePath: string = AnalyzerUtils.getWorkspacePath(fileScanBundle.dataNode, descriptorIssues.fullPath);
if (!workspaceToScanBundles.has(workspacePath)) {
workspaceToScanBundles.set(workspacePath, new Map<FileScanBundle, Set<string>>());
workspaceToScanBundles.set(workspacePath, new BundleCves());
}
workspaceToScanBundles.get(workspacePath)?.set(fileScanBundle, cvesToScan);
workspaceToScanBundles.get(workspacePath)?.set(fileScanBundle, cvesTuple);
this._logManager.logMessage('Adding data from descriptor ' + descriptorIssues.fullPath + ' for cve applicability scan', 'INFO');
}

Expand Down Expand Up @@ -244,6 +273,7 @@ export class ApplicabilityRunner extends JasRunner {
// Prepare
const analyzerScanRun: AnalyzerScanRun = response.runs[0];
const applicable: Map<string, CveApplicableDetails> = new Map<string, CveApplicableDetails>();
const nonapplicable: string[] = [];
const scanned: Set<string> = new Set<string>();
const rulesFullDescription: Map<string, string> = new Map<string, string>();
for (const rule of analyzerScanRun.tool.driver.rules) {
Expand All @@ -266,13 +296,17 @@ export class ApplicabilityRunner extends JasRunner {
fileIssues.locations.push(location.physicalLocation.region);
});
}
else if (analyzeIssue.kind === 'pass') {
nonapplicable.push(this.getCveFromRuleId(analyzeIssue.ruleId));
}
scanned.add(this.getCveFromRuleId(analyzeIssue.ruleId));
});
}
// Convert data to a response
return {
scannedCve: Array.from(scanned),
applicableCve: Object.fromEntries(applicable.entries())
applicableCve: Object.fromEntries(applicable.entries()),
nonapplicableCve: nonapplicable,
} as ApplicabilityScanResponse;
}

Expand Down
8 changes: 7 additions & 1 deletion src/main/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ export class Utils {
}

public static combineSets(sets: Set<string>[]): Set<string> {
return new Set<string>(...sets);
const result: Set<string> = new Set<string>;
for (const set of sets) {
for (const elem of set) {
result.add(elem);
}
}
return result;
}

/**
Expand Down
10 changes: 9 additions & 1 deletion src/test/resources/applicableScan/npm/expectedScanResponse.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"CVE-2021-3807",
"CVE-2021-3918"
],
"indirectCve": [
"CVE-2021-44228"
],
"applicableCve": {
"CVE-2022-25878": {
"fixReason": "Prototype pollution `Object.freeze` remediation was not detected, The vulnerable function protobufjs.parse is called with external input, The vulnerable function protobufjs.load(Sync) is called",
Expand Down Expand Up @@ -34,5 +37,10 @@
],
"fullDescription": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `util.setProperty` with external input to its 2nd (`path`) or 3rd (`value`) arguments.\n* `ReflectionObject.setParsedOption` with external input to its 2nd (`name`) or 3rd (`value`) arguments.\n* `parse` with external input to its 1st (`source`) argument.\n* `load`\n* `loadSync`\n\nThe scanner also checks whether the `Object.freeze()` remediation is not present."
}
}
},
"nonapplicableCve": [
"CVE-2021-3807",
"CVE-2021-3918",
"CVE-2021-44228"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"CVE-2019-15605",
"CVE-2019-20907"
],
"indirectCve": [],
"applicableCve": {
"CVE-2019-20907": {
"fixReason": "The vulnerable function tarfile.open is called with external input",
Expand All @@ -25,5 +26,9 @@
],
"fullDescription": "The scanner checks whether the vulnerable function `open` is called with external input to its 1st (`name`) argument."
}
}
},
"nonapplicableCve": [
"CVE-2021-3918",
"CVE-2019-15605"
]
}
3 changes: 3 additions & 0 deletions src/test/resources/applicableScan/requestMultipleRoots.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ scans:
- CVE-2021-3918
- CVE-2021-3807
- CVE-2022-25878
indirect-cve-whitelist:
- CVE-2021-44228
- CVE-2023-1234
skipped-folders:
- /path/to/skip
2 changes: 2 additions & 0 deletions src/test/resources/applicableScan/requestOneRoot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ scans:
- /path/to/root
cve-whitelist:
- CVE-2021-3918
indirect-cve-whitelist:
- CVE-2021-44228
skipped-folders: []
7 changes: 5 additions & 2 deletions src/test/tests/applicabilityScan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,20 @@ describe('Applicability Scan Tests', () => {
expectedYaml: path.join(scanApplicable, 'requestOneRoot.yaml'),
roots: ['/path/to/root'],
cves: ['CVE-2021-3918'],
indirect_cves: ['CVE-2021-44228'],
skip: []
},
{
name: 'Multiple roots',
expectedYaml: path.join(scanApplicable, 'requestMultipleRoots.yaml'),
roots: ['/path/to/root', '/path/to/other'],
cves: ['CVE-2021-3918', 'CVE-2021-3807', 'CVE-2022-25878'],
indirect_cves: ['CVE-2021-44228', 'CVE-2023-1234'],
skip: ['/path/to/skip']
}
].forEach(test => {
it('Check generated Yaml request for - ' + test.name, () => {
let request: ApplicabilityScanArgs = getApplicabilityScanRequest(test.roots, test.cves, test.skip);
let request: ApplicabilityScanArgs = getApplicabilityScanRequest(test.roots, test.cves, test.indirect_cves, test.skip);
let actualYaml: string = path.join(tempFolder, test.name);
fs.writeFileSync(actualYaml, getDummyRunner([], PackageType.Unknown).requestsToYaml(request));
assert.deepEqual(
Expand Down Expand Up @@ -345,12 +347,13 @@ describe('Applicability Scan Tests', () => {
});
});

function getApplicabilityScanRequest(roots: string[], cves: string[], skipFolders: string[]): ApplicabilityScanArgs {
function getApplicabilityScanRequest(roots: string[], cves: string[], indirect_cves: string[], skipFolders: string[]): ApplicabilityScanArgs {
return {
type: 'analyze-applicability',
output: '/path/to/output.json',
roots: roots,
cve_whitelist: cves,
indirect_cve_whitelist: indirect_cves,
skipped_folders: skipFolders
} as ApplicabilityScanArgs;
}
Expand Down
6 changes: 5 additions & 1 deletion src/test/tests/integration/applicability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('Applicability Integration Tests', async () => {
assert.isDefined(expectedContent, 'Failed to read expected ApplicabilityScanResponse content from ' + expectedResponseContentPath);
// Run scan
response = await runner
.executeRequest(() => undefined, { roots: [directoryToScan], cve_whitelist: expectedContent.scannedCve } as ApplicabilityScanArgs)
.executeRequest(() => undefined, { roots: [directoryToScan], cve_whitelist: expectedContent.scannedCve, indirect_cve_whitelist: expectedContent.indirectCve } as ApplicabilityScanArgs)
.then(runResult => runner.convertResponse(runResult))
.catch(err => assert.fail(err));
});
Expand All @@ -67,6 +67,10 @@ describe('Applicability Integration Tests', async () => {
assert.includeDeepMembers(Object.keys(response.applicableCve), Object.keys(expectedContent.applicableCve));
});

it('Check all expected nonapplicable CVE detected', () => {
assert.sameDeepMembers(response.nonapplicableCve, expectedContent.nonapplicableCve);
});

describe('Applicable details data validations', () => {
let expectedApplicableCves: Map<string, CveApplicableDetails>;
let responseApplicableCves: Map<string, CveApplicableDetails>;
Expand Down

0 comments on commit 1a9ae97

Please sign in to comment.