diff --git a/README.md b/README.md index db2c52c..0039ac6 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,11 @@ following keys for setting custom paths for the said executables. EXHORT_MVN_PATH +Maven +maven +EXHORT_PREFER_MVNW + + NPM npm EXHORT_NPM_PATH @@ -385,6 +390,11 @@ following keys for setting custom paths for the said executables. gradle EXHORT_GRADLE_PATH + +Gradle +gradle +EXHORT_PREFER_GRADLEW + #### Match Manifest Versions Feature @@ -463,7 +473,7 @@ Need to set environment variable/option - `EXHORT_PIP_USE_DEP_TREE` to true. ### Known Issues - For pip requirements.txt - It's been observed that for python versions 3.11.x, there might be slowness for invoking the analysis. - If you encounter a performance issue with version >= 3.11.x, kindly try to set environment variable/option `EXHORT_PIP_USE_DEP_TREE`=true, before calling the analysis. + If you encounter a performance issue with version >= 3.11.x, kindly try to set environment variable/option `EXHORT_PIP_USE_DEP_TREE=true`, before calling the analysis. - For maven pom.xml, it has been noticed that using java 17 might cause stack analysis to hang forever. diff --git a/src/providers/base_java.js b/src/providers/base_java.js index c9cbde8..c626104 100644 --- a/src/providers/base_java.js +++ b/src/providers/base_java.js @@ -1,6 +1,7 @@ import { PackageURL } from 'packageurl-js' - -import { invokeCommand } from "../tools.js" +import { getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js" +import fs from 'node:fs' +import path from 'node:path' /** @typedef {import('../provider').Provider} */ @@ -21,6 +22,19 @@ export default class Base_Java { DEP_REGEX = /(([-a-zA-Z0-9._]{2,})|[0-9])/g CONFLICT_REGEX = /.*omitted for conflict with (\S+)\)/ + globalBinary + localWrapper + + /** + * + * @param {string} globalBinary name of the global binary + * @param {string} localWrapper name of the local wrapper filename + */ + constructor(globalBinary, localWrapper) { + this.globalBinary = globalBinary + this.localWrapper = localWrapper + } + /** * Recursively populates the SBOM instance with the parsed graph * @param {string} src - Source dependency to start the calculations from @@ -107,10 +121,72 @@ export default class Base_Java { return new PackageURL('maven', group, artifact, version, undefined, undefined); } - /** this method invokes command string in a process in a synchronous way. + /** This method invokes command string in a process in a synchronous way. + * Exists for stubbing in tests. * @param bin - the command to be invoked * @param args - the args to pass to the binary * @protected */ _invokeCommand(bin, args, opts={}) { return invokeCommand(bin, args, opts) } + + /** + * + * @param {string} manifestPath + * @param {{}} opts + * @returns string + */ + selectToolBinary(manifestPath, opts) { + const toolPath = getCustomPath(this.globalBinary, opts) + + const useWrapper = getWrapperPreference(toolPath, opts) + if (useWrapper) { + const wrapper = this.traverseForWrapper(manifestPath) + if (wrapper !== undefined) { + try { + this._invokeCommand(wrapper, ['--version']) + } catch (error) { + throw new Error(`failed to check for ${this.localWrapper}`, {cause: error}) + } + return wrapper + } + } + // verify tool is accessible, if wrapper was not requested or not found + try { + this._invokeCommand(toolPath, ['--version']) + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error((useWrapper ? `${this.localWrapper} not found and ` : '') + `${this.globalBinary === 'mvn' ? 'maven' : 'gradle'} not found at ${toolPath}`) + } else { + throw new Error(`failed to check for ${this.globalBinary === 'mvn' ? 'maven' : 'gradle'}`, {cause: error}) + } + } + return toolPath + } + + /** + * + * @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper + * @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback + * to the root of the drive the manifest is on (assumes absolute path is given) + * @returns {string|undefined} + */ + traverseForWrapper(startingManifest, repoRoot = undefined) { + repoRoot = repoRoot || getGitRootDir(path.resolve(path.dirname(startingManifest))) || path.parse(path.resolve(startingManifest)).root + + const wrapperName = this.localWrapper; + const wrapperPath = path.join(path.resolve(path.dirname(startingManifest)), wrapperName); + + try { + fs.accessSync(wrapperPath, fs.constants.X_OK) + } catch(error) { + if (error.code === 'ENOENT') { + if (path.resolve(path.dirname(startingManifest)) === repoRoot) { + return undefined + } + return this.traverseForWrapper(path.resolve(path.dirname(startingManifest)), repoRoot) + } + throw new Error(`failure searching for ${this.localWrapper}`, {cause: error}) + } + return wrapperPath + } } diff --git a/src/providers/java_gradle.js b/src/providers/java_gradle.js index 5b8e4a6..4b01fc1 100644 --- a/src/providers/java_gradle.js +++ b/src/providers/java_gradle.js @@ -1,5 +1,4 @@ import fs from 'node:fs' -import {getCustomPath} from "../tools.js"; import path from 'node:path' import Sbom from '../sbom.js' import { EOL } from 'os' @@ -88,6 +87,9 @@ const stackAnalysisConfigs = ["runtimeClasspath","compileClasspath"]; * This class provides common functionality for Groovy and Kotlin DSL files. */ export default class Java_gradle extends Base_java { + constructor() { + super('gradle', 'gradlew' + (process.platform === 'win32' ? '.bat' : '')) + } _getManifestName() { throw new Error('implement getManifestName method') @@ -154,7 +156,7 @@ export default class Java_gradle extends Base_java { * @private */ #createSbomStackAnalysis(manifest, opts = {}) { - let content = this.#getDependencies(manifest) + let content = this.#getDependencies(manifest, opts) let properties = this.#extractProperties(manifest, opts) // read dependency tree from temp file if (process.env["EXHORT_DEBUG"] === "true") { @@ -192,7 +194,7 @@ export default class Java_gradle extends Base_java { * @return {string} string content of the properties */ #getProperties(manifestPath, opts) { - let gradle = getCustomPath("gradle", opts); + let gradle = this.selectToolBinary(manifestPath, opts) try { let properties = this._invokeCommand(gradle, ['properties'], {cwd: path.dirname(manifestPath)}) return properties.toString() @@ -208,7 +210,7 @@ export default class Java_gradle extends Base_java { * @private */ #getSbomForComponentAnalysis(manifestPath, opts = {}) { - let content = this.#getDependencies(manifestPath) + let content = this.#getDependencies(manifestPath, opts) let properties = this.#extractProperties(manifestPath, opts) let configurationNames = componentAnalysisConfigs @@ -234,8 +236,8 @@ export default class Java_gradle extends Base_java { * @private */ - #getDependencies(manifest) { - const gradle = getCustomPath("gradle") + #getDependencies(manifest, opts={}) { + const gradle = this.selectToolBinary(manifest, opts) try { const commandResult = this._invokeCommand(gradle, ['dependencies'], {cwd: path.dirname(manifest)}) return commandResult.toString() diff --git a/src/providers/java_maven.js b/src/providers/java_maven.js index 9665d14..33809a3 100644 --- a/src/providers/java_maven.js +++ b/src/providers/java_maven.js @@ -1,6 +1,5 @@ import { XMLParser } from 'fast-xml-parser' import fs from 'node:fs' -import { getCustomPath, getGitRootDir, getWrapperPreference } from "../tools.js"; import os from 'node:os' import path from 'node:path' import Sbom from '../sbom.js' @@ -17,6 +16,9 @@ import Base_java, { ecosystem_maven } from "./base_java.js"; /** @typedef {{groupId: string, artifactId: string, version: string, scope: string, ignore: boolean}} Dependency */ export default class Java_maven extends Base_java { + constructor() { + super('mvn', 'mvnw' + (process.platform === 'win32' ? '.cmd' : '')) + } /** * @param {string} manifestName - the subject manifest name-type @@ -71,7 +73,7 @@ export default class Java_maven extends Base_java { * @private */ #createSbomStackAnalysis(manifest, opts = {}) { - const mvn = this.#selectMvnRuntime(manifest, opts) + const mvn = this.selectToolBinary(manifest, opts) // clean maven target try { @@ -136,7 +138,7 @@ export default class Java_maven extends Base_java { * @private */ #getSbomForComponentAnalysis(manifestPath, opts = {}) { - const mvn = this.#selectMvnRuntime(manifestPath, opts) + const mvn = this.selectToolBinary(manifestPath, opts) const tmpEffectivePom = path.resolve(path.join(path.dirname(manifestPath), 'effective-pom.xml')) const targetPom = manifestPath @@ -201,63 +203,6 @@ export default class Java_maven extends Base_java { return rootDependency } - #selectMvnRuntime(manifestPath, opts) { - // get custom maven path - let mvn = getCustomPath('mvn', opts) - - // check if mvnw is preferred and available - let useMvnw = getWrapperPreference('mvn', opts) - if (useMvnw) { - const mvnw = this.#traverseForMvnw(manifestPath) - if (mvnw !== undefined) { - try { - this._invokeCommand(mvnw, ['--version']) - } catch (error) { - throw new Error(`failed to check for mvnw`, {cause: error}) - } - return mvnw - } - } - // verify maven is accessible, if mvnw was not requested or not found - try { - this._invokeCommand(mvn, ['--version']) - } catch (error) { - if (error.code === 'ENOENT') { - throw new Error((useMvnw ? 'mvnw not found and ' : '') + `maven not accessible at "${mvn}"`) - } else { - throw new Error(`failed to check for maven`, {cause: error}) - } - } - return mvn - } - - /** - * - * @param {string} startingManifest - the path of the manifest from which to start searching for mvnw - * @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback - * to the root of the drive the manifest is on (assumes absolute path is given) - * @returns - */ - #traverseForMvnw(startingManifest, repoRoot = undefined) { - repoRoot = repoRoot || getGitRootDir(path.resolve(path.dirname(startingManifest))) || path.parse(path.resolve(startingManifest)).root - - const wrapperName = 'mvnw' + (process.platform === 'win32' ? '.cmd' : ''); - const wrapperPath = path.join(path.resolve(path.dirname(startingManifest)), wrapperName); - - try { - fs.accessSync(wrapperPath, fs.constants.X_OK) - } catch(error) { - if (error.code === 'ENOENT') { - if (path.resolve(path.dirname(startingManifest)) === repoRoot) { - return undefined - } - return this.#traverseForMvnw(path.resolve(path.dirname(startingManifest)), repoRoot) - } - throw new Error(`failure searching for mvnw`, {cause: error}) - } - return wrapperPath - } - /** * Get a list of dependencies with marking of dependencies commented with . * @param {string} manifest - path for pom.xml diff --git a/test/it/end-to-end.js b/test/it/end-to-end.js index a5a34a8..12aa915 100644 --- a/test/it/end-to-end.js +++ b/test/it/end-to-end.js @@ -52,7 +52,7 @@ suite('Integration Tests', () => { let pomPath = `test/it/test_manifests/${packageManager}/${manifestName}` let providedDataForStack = await index.stackAnalysis(pomPath) console.log(JSON.stringify(providedDataForStack,null , 4)) - let providers = ["osv"] + let providers = ["tpa"] providers.forEach(provider => expect(extractTotalsGeneralOrFromProvider(providedDataForStack, provider)).greaterThan(0)) // TODO: if sources doesn't exist, add "scanned" instead // python transitive count for stack analysis is awaiting fix in exhort backend @@ -84,7 +84,7 @@ suite('Integration Tests', () => { reportParsedFromHtml = JSON.parse("{" + startOfJson.substring(0,startOfJson.indexOf("};") + 1)) reportParsedFromHtml = reportParsedFromHtml.report } finally { - parsedStatusFromHtmlOsvNvd = reportParsedFromHtml.providers["osv"].status + parsedStatusFromHtmlOsvNvd = reportParsedFromHtml.providers["tpa"].status expect(parsedStatusFromHtmlOsvNvd.code).equals(200) parsedScannedFromHtml = reportParsedFromHtml.scanned expect( typeof html).equals("string") @@ -101,7 +101,7 @@ suite('Integration Tests', () => { expect(analysisReport.scanned.total).greaterThan(0) expect(analysisReport.scanned.transitive).equal(0) - let providers = ["osv"] + let providers = ["tpa"] providers.forEach(provider => expect(extractTotalsGeneralOrFromProvider(analysisReport, provider)).greaterThan(0)) providers.forEach(provider => expect(analysisReport.providers[provider].status.code).equals(200)) }).timeout(20000);