Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ following keys for setting custom paths for the said executables.
<td>EXHORT_MVN_PATH</td>
</tr>
<tr>
<td><a href="https://maven.apache.org/">Maven</a></td>
<td><em>maven</em></td>
<td>EXHORT_PREFER_MVNW</td>
</tr>
<tr>
<td><a href="https://www.npmjs.com/">NPM</a></td>
<td><em>npm</em></td>
<td>EXHORT_NPM_PATH</td>
Expand Down Expand Up @@ -385,6 +390,11 @@ following keys for setting custom paths for the said executables.
<td><em>gradle</em></td>
<td>EXHORT_GRADLE_PATH</td>
</tr>
<tr>
<td><a href="https://gradle.org/">Gradle</a></td>
<td><em>gradle</em></td>
<td>EXHORT_PREFER_GRADLEW</td>
</tr>
</table>

#### Match Manifest Versions Feature
Expand Down Expand Up @@ -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.
Expand Down
82 changes: 79 additions & 3 deletions src/providers/base_java.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PackageURL } from 'packageurl-js'

import { invokeCommand } from "../tools.js"
import { getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js"

Check warning on line 2 in src/providers/base_java.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Expected 'multiple' syntax before 'single' syntax

Check warning on line 2 in src/providers/base_java.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Expected 'multiple' syntax before 'single' syntax
import fs from 'node:fs'
import path from 'node:path'


/** @typedef {import('../provider').Provider} */
Expand All @@ -21,6 +22,19 @@
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
Expand Down Expand Up @@ -107,10 +121,72 @@
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
}
}
14 changes: 8 additions & 6 deletions src/providers/java_gradle.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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()
Expand All @@ -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

Expand All @@ -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()
Expand Down
65 changes: 5 additions & 60 deletions src/providers/java_maven.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <!--exhortignore-->.
* @param {string} manifest - path for pom.xml
Expand Down
6 changes: 3 additions & 3 deletions test/it/end-to-end.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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);
Expand Down
Loading