@trustify-da/trustify-da-javascript-client 0.3.0-ea.d71f957 → 0.3.0-ea.d8ec262

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +56 -2
  2. package/dist/package.json +2 -2
  3. package/dist/src/analysis.js +3 -2
  4. package/dist/src/cli.js +51 -2
  5. package/dist/src/cyclone_dx_sbom.d.ts +7 -1
  6. package/dist/src/cyclone_dx_sbom.js +29 -8
  7. package/dist/src/index.d.ts +74 -3
  8. package/dist/src/index.js +92 -7
  9. package/dist/src/oci_image/utils.js +11 -2
  10. package/dist/src/provider.d.ts +3 -2
  11. package/dist/src/provider.js +9 -1
  12. package/dist/src/providers/base_java.d.ts +12 -10
  13. package/dist/src/providers/base_java.js +18 -44
  14. package/dist/src/providers/base_javascript.d.ts +11 -0
  15. package/dist/src/providers/base_javascript.js +10 -3
  16. package/dist/src/providers/base_pyproject.d.ts +158 -0
  17. package/dist/src/providers/base_pyproject.js +322 -0
  18. package/dist/src/providers/golang_gomodules.d.ts +10 -0
  19. package/dist/src/providers/golang_gomodules.js +65 -8
  20. package/dist/src/providers/java_gradle.d.ts +19 -0
  21. package/dist/src/providers/java_gradle.js +116 -2
  22. package/dist/src/providers/java_maven.d.ts +8 -0
  23. package/dist/src/providers/java_maven.js +165 -1
  24. package/dist/src/providers/javascript_bun.d.ts +10 -0
  25. package/dist/src/providers/javascript_bun.js +100 -0
  26. package/dist/src/providers/javascript_npm.d.ts +1 -0
  27. package/dist/src/providers/javascript_npm.js +21 -0
  28. package/dist/src/providers/javascript_pnpm.js +6 -2
  29. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  30. package/dist/src/providers/marker_evaluator.js +191 -0
  31. package/dist/src/providers/processors/yarn_berry_processor.js +6 -2
  32. package/dist/src/providers/python_controller.d.ts +5 -1
  33. package/dist/src/providers/python_controller.js +65 -7
  34. package/dist/src/providers/python_pip.d.ts +5 -0
  35. package/dist/src/providers/python_pip.js +5 -5
  36. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  37. package/dist/src/providers/python_pip_pyproject.js +146 -0
  38. package/dist/src/providers/python_poetry.d.ts +97 -0
  39. package/dist/src/providers/python_poetry.js +271 -0
  40. package/dist/src/providers/python_uv.d.ts +55 -0
  41. package/dist/src/providers/python_uv.js +227 -0
  42. package/dist/src/providers/rust_cargo.d.ts +5 -1
  43. package/dist/src/providers/rust_cargo.js +31 -4
  44. package/dist/src/sbom.d.ts +7 -1
  45. package/dist/src/sbom.js +4 -2
  46. package/dist/src/tools.d.ts +26 -0
  47. package/dist/src/tools.js +58 -0
  48. package/dist/src/workspace.d.ts +9 -0
  49. package/dist/src/workspace.js +1 -1
  50. package/package.json +3 -3
package/README.md CHANGED
@@ -224,7 +224,8 @@ $ trustify-da-javascript-client license /path/to/package.json
224
224
  <li><a href="https://www.javascript.com/">JavaScript</a> - <a href="https://pnpm.io/">pnpm</a></li>
225
225
  <li><a href="https://www.javascript.com/">JavaScript</a> - <a href="https://classic.yarnpkg.com/">Yarn Classic</a> / <a href="https://yarnpkg.com/">Yarn Berry</a></li>
226
226
  <li><a href="https://go.dev/">Golang</a> - <a href="https://go.dev/blog/using-go-modules/">Go Modules</a></li>
227
- <li><a href="https://www.python.org/">Python</a> - <a href="https://pypi.org/project/pip/">pip Installer</a></li>
227
+ <li><a href="https://www.python.org/">Python</a> - <a href="https://pypi.org/project/pip/">pip Installer</a> (<code>requirements.txt</code>)</li>
228
+ <li><a href="https://www.python.org/">Python</a> - <a href="https://python-poetry.org/">Poetry</a> / <a href="https://docs.astral.sh/uv/">uv</a> (<code>pyproject.toml</code>)</li>
228
229
  <li><a href="https://gradle.org/">Gradle (Groovy and Kotlin DSL)</a> - <a href="https://gradle.org/install/">Gradle Installation</a></li>
229
230
  <li><a href="https://www.rust-lang.org/">Rust</a> - <a href="https://doc.rust-lang.org/cargo/">Cargo</a></li>
230
231
  </ul>
@@ -391,7 +392,30 @@ version = "1.10"
391
392
  log = "0.4" # trustify-da-ignore
392
393
  ```
393
394
 
394
- All of the 6 above examples are valid for marking a package to be ignored
395
+
396
+ <em>Python pyproject.toml</em> users can add a comment with <code>#exhortignore</code> (or <code># trustify-da-ignore</code>) next to the dependency in <code>pyproject.toml</code>.
397
+
398
+ PEP 621 style (<code>[project]</code> dependencies):
399
+ ```toml
400
+ [project]
401
+ dependencies = [
402
+ "flask>=2.0.3",
403
+ "requests>=2.25.1",
404
+ "uvicorn>=0.17.0", #exhortignore
405
+ "click>=8.0.4", # trustify-da-ignore
406
+ ]
407
+ ```
408
+
409
+ Poetry style (<code>[tool.poetry.dependencies]</code>):
410
+ ```toml
411
+ [tool.poetry.dependencies]
412
+ flask = "^2.0.3"
413
+ requests = "^2.25.1"
414
+ uvicorn = "^0.17.0" #exhortignore
415
+ click = "^8.0.4" # trustify-da-ignore
416
+ ```
417
+
418
+ All of the above examples are valid for marking a package to be ignored
395
419
  </li>
396
420
  </ul>
397
421
 
@@ -421,6 +445,8 @@ let options = {
421
445
  'TRUSTIFY_DA_PIP3_PATH' : '/path/to/pip3',
422
446
  'TRUSTIFY_DA_PYTHON_PATH' : '/path/to/python',
423
447
  'TRUSTIFY_DA_PIP_PATH' : '/path/to/pip',
448
+ 'TRUSTIFY_DA_UV_PATH' : '/path/to/uv',
449
+ 'TRUSTIFY_DA_POETRY_PATH' : '/path/to/poetry',
424
450
  'TRUSTIFY_DA_GRADLE_PATH' : '/path/to/gradle',
425
451
  'TRUSTIFY_DA_CARGO_PATH' : '/path/to/cargo',
426
452
  // Workspace root for monorepos (Cargo, npm/pnpm/yarn); lock file expected here
@@ -567,6 +593,16 @@ following keys for setting custom paths for the said executables.
567
593
  <td>TRUSTIFY_DA_CARGO_PATH</td>
568
594
  </tr>
569
595
  <tr>
596
+ <td><a href="https://docs.astral.sh/uv/">uv</a></td>
597
+ <td><em>uv</em></td>
598
+ <td>TRUSTIFY_DA_UV_PATH</td>
599
+ </tr>
600
+ <tr>
601
+ <td><a href="https://python-poetry.org/">Poetry</a></td>
602
+ <td><em>poetry</em></td>
603
+ <td>TRUSTIFY_DA_POETRY_PATH</td>
604
+ </tr>
605
+ <tr>
570
606
  <td>Workspace root (monorepos)</td>
571
607
  <td>—</td>
572
608
  <td>workspaceDir / TRUSTIFY_DA_WORKSPACE_DIR</td>
@@ -619,6 +655,24 @@ TRUSTIFY_DA_GO_MVS_LOGIC_ENABLED=false
619
655
 
620
656
  #### Python Support
621
657
 
658
+ The client supports two Python manifest formats:
659
+
660
+ - **`requirements.txt`** — uses pip/pip3 to resolve dependencies
661
+ - **`pyproject.toml`** — uses [uv](https://docs.astral.sh/uv/) or [Poetry](https://python-poetry.org/) to resolve dependencies
662
+
663
+ ##### pyproject.toml
664
+
665
+ For `pyproject.toml` projects, the client detects which tool manages the project by checking for lock files:
666
+ - If `poetry.lock` is present and `[tool.poetry]` is defined, **Poetry** is used (`poetry show --tree` and `poetry show --all`)
667
+ - If `uv.lock` is present, **uv** is used (`uv export --format requirements.txt --frozen --no-hashes`)
668
+ - If neither lock file is found, an error is thrown
669
+
670
+ Both PEP 621 (`[project]` dependencies) and Poetry-style (`[tool.poetry.dependencies]`) are supported.
671
+
672
+ Custom executable paths can be set via `TRUSTIFY_DA_UV_PATH` and `TRUSTIFY_DA_POETRY_PATH`.
673
+
674
+ ##### requirements.txt
675
+
622
676
  By default, For python support, the api assumes that the package is installed using the pip/pip3 binary on the system PATH, or using the customized
623
677
  Binaries passed to environment variables. In any case, If the package is not installed , then an error will be thrown.
624
678
 
package/dist/package.json CHANGED
@@ -38,7 +38,7 @@
38
38
  "lint": "eslint src test --ext js",
39
39
  "lint:fix": "eslint src test --ext js --fix",
40
40
  "test": "c8 npm run tests",
41
- "tests": "mocha --config .mocharc.json --grep \".*analysis module.*\" --invert",
41
+ "tests": "mocha --config .mocharc.json",
42
42
  "tests:rep": "mocha --reporter-option maxDiffSize=0 --reporter json > unit-tests-result.json",
43
43
  "pretest": "cp node_modules/tree-sitter-requirements/tree-sitter-requirements.wasm src/providers/tree-sitter-requirements.wasm && cp node_modules/tree-sitter-gomod/tree-sitter-gomod.wasm src/providers/tree-sitter-gomod.wasm",
44
44
  "precompile": "rm -rf dist",
@@ -51,11 +51,11 @@
51
51
  "@cyclonedx/cyclonedx-library": "^6.13.0",
52
52
  "eslint-import-resolver-typescript": "^4.4.4",
53
53
  "fast-glob": "^3.3.3",
54
- "fast-toml": "^0.5.4",
55
54
  "fast-xml-parser": "^5.3.4",
56
55
  "help": "^3.0.2",
57
56
  "https-proxy-agent": "^7.0.6",
58
57
  "js-yaml": "^4.1.1",
58
+ "jsonc-parser": "^3.3.1",
59
59
  "micromatch": "^4.0.8",
60
60
  "node-fetch": "^3.3.2",
61
61
  "p-limit": "^4.0.0",
@@ -189,7 +189,7 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
189
189
  if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
190
190
  finalUrl.searchParams.append('recommend', 'false');
191
191
  }
192
- const resp = await fetch(finalUrl, {
192
+ const fetchOptions = addProxyAgent({
193
193
  method: 'POST',
194
194
  headers: {
195
195
  'Accept': html ? 'text/html' : 'application/json',
@@ -197,7 +197,8 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
197
197
  ...getTokenHeaders(opts)
198
198
  },
199
199
  body: JSON.stringify(imageSboms),
200
- });
200
+ }, opts);
201
+ const resp = await fetch(finalUrl, fetchOptions);
201
202
  if (resp.status === 200) {
202
203
  let result;
203
204
  if (!html) {
package/dist/src/cli.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
2
3
  import * as path from "path";
3
4
  import yargs from 'yargs';
4
5
  import { hideBin } from 'yargs/helpers';
5
6
  import { getProjectLicense, getLicenseDetails } from './license/index.js';
6
- import client, { selectTrustifyDABackend } from './index.js';
7
+ import client, { selectTrustifyDABackend, generateSbom } from './index.js';
7
8
  // command for component analysis take manifest type and content
8
9
  const component = {
9
10
  command: 'component </path/to/manifest>',
@@ -325,15 +326,63 @@ const license = {
325
326
  console.log(JSON.stringify(output, null, 2));
326
327
  }
327
328
  };
329
+ const sbom = {
330
+ command: 'sbom </path/to/manifest> [--output]',
331
+ desc: 'generate a CycloneDX SBOM from a manifest file',
332
+ builder: yargs => yargs.positional('/path/to/manifest', {
333
+ desc: 'manifest path for SBOM generation',
334
+ type: 'string',
335
+ normalize: true,
336
+ }).options({
337
+ output: {
338
+ alias: 'o',
339
+ desc: 'Write SBOM JSON to a file instead of stdout',
340
+ type: 'string',
341
+ normalize: true,
342
+ },
343
+ workspaceDir: {
344
+ alias: 'w',
345
+ desc: 'Workspace root directory (for monorepos; lock file is expected here)',
346
+ type: 'string',
347
+ normalize: true,
348
+ }
349
+ }),
350
+ handler: async (args) => {
351
+ let manifest = args['/path/to/manifest'];
352
+ const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
353
+ let result;
354
+ try {
355
+ result = await generateSbom(manifest, opts);
356
+ }
357
+ catch (err) {
358
+ console.error(JSON.stringify({ error: `Failed to generate SBOM: ${err.message}` }, null, 2));
359
+ process.exit(1);
360
+ }
361
+ const json = JSON.stringify(result, null, 2);
362
+ if (args.output) {
363
+ try {
364
+ fs.writeFileSync(args.output, json);
365
+ }
366
+ catch (err) {
367
+ console.error(JSON.stringify({ error: `Failed to write output file: ${err.message}` }, null, 2));
368
+ process.exit(1);
369
+ }
370
+ }
371
+ else {
372
+ console.log(json);
373
+ }
374
+ }
375
+ };
328
376
  // parse and invoke the command
329
377
  yargs(hideBin(process.argv))
330
- .usage(`Usage: ${process.argv[0].includes("node") ? path.parse(process.argv[1]).base : path.parse(process.argv[0]).base} {component|stack|stack-batch|image|validate-token|license}`)
378
+ .usage(`Usage: ${process.argv[0].includes("node") ? path.parse(process.argv[1]).base : path.parse(process.argv[0]).base} {component|stack|stack-batch|image|validate-token|license|sbom}`)
331
379
  .command(stack)
332
380
  .command(stackBatch)
333
381
  .command(component)
334
382
  .command(image)
335
383
  .command(validateToken)
336
384
  .command(license)
385
+ .command(sbom)
337
386
  .scriptName('')
338
387
  .version(false)
339
388
  .demandCommand(1)
@@ -18,9 +18,14 @@ export default class CycloneDxSbom {
18
18
  * Adds a dependency relationship between two components in the SBOM
19
19
  * @param {PackageURL} sourceRef - The source component (parent)
20
20
  * @param {PackageURL} targetRef - The target component (dependency)
21
+ * @param {string} [scope] - Scope of the dependency
22
+ * @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
21
23
  * @return {CycloneDxSbom} The updated SBOM
22
24
  */
23
- addDependency(sourceRef: PackageURL, targetRef: PackageURL, scope: any): CycloneDxSbom;
25
+ addDependency(sourceRef: PackageURL, targetRef: PackageURL, scope?: string, targetHashes?: Array<{
26
+ alg: string;
27
+ content: string;
28
+ }>): CycloneDxSbom;
24
29
  /** @param {{}} opts - various options, settings and configuration of application.
25
30
  * @return String CycloneDx Sbom json object in a string format
26
31
  */
@@ -50,6 +55,7 @@ export default class CycloneDxSbom {
50
55
  version: any;
51
56
  scope: any;
52
57
  licenses?: any;
58
+ hashes?: any;
53
59
  };
54
60
  /**
55
61
  * This method gets an array of dependencies to be ignored, and remove all of them from CycloneDx Sbom
@@ -6,10 +6,11 @@ import { PackageURL } from "packageurl-js";
6
6
  * @param type type of package - application or library
7
7
  * @param scope scope of the component - runtime or compile
8
8
  * @param licenses optional license string or array of licenses for the component
9
- * @return {{"bom-ref": string, name, purl: string, type, version, scope, licenses?}}
9
+ * @param hashes optional array of hash objects for the component, e.g. [{alg: "SHA-256", content: "..."}]
10
+ * @return {{"bom-ref": string, name, purl: string, type, version, scope, licenses?, hashes?}}
10
11
  * @private
11
12
  */
12
- function getComponent(component, type, scope, licenses) {
13
+ function getComponent(component, type, scope, licenses, hashes) {
13
14
  let componentObject;
14
15
  if (component instanceof PackageURL) {
15
16
  if (component.namespace) {
@@ -40,12 +41,24 @@ function getComponent(component, type, scope, licenses) {
40
41
  // Add licenses if provided (CycloneDX format). Callers must provide valid SPDX identifiers.
41
42
  if (licenses) {
42
43
  const licenseArray = Array.isArray(licenses) ? licenses : [licenses];
43
- componentObject.licenses = licenseArray.map(lic => {
44
+ const validLicenses = licenseArray
45
+ .map(lic => {
44
46
  if (typeof lic === 'string') {
45
47
  return { license: { id: lic } };
46
48
  }
47
- return lic;
48
- });
49
+ if (typeof lic === 'object' && lic !== null && ('license' in lic || 'expression' in lic)) {
50
+ return lic;
51
+ }
52
+ return null;
53
+ })
54
+ .filter(Boolean);
55
+ if (validLicenses.length > 0) {
56
+ componentObject.licenses = validLicenses;
57
+ }
58
+ }
59
+ // Add hashes if provided (CycloneDX 1.4 format).
60
+ if (hashes && hashes.length > 0) {
61
+ componentObject.hashes = hashes;
49
62
  }
50
63
  return componentObject;
51
64
  }
@@ -86,16 +99,24 @@ export default class CycloneDxSbom {
86
99
  * Adds a dependency relationship between two components in the SBOM
87
100
  * @param {PackageURL} sourceRef - The source component (parent)
88
101
  * @param {PackageURL} targetRef - The target component (dependency)
102
+ * @param {string} [scope] - Scope of the dependency
103
+ * @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
89
104
  * @return {CycloneDxSbom} The updated SBOM
90
105
  */
91
- addDependency(sourceRef, targetRef, scope) {
106
+ addDependency(sourceRef, targetRef, scope, targetHashes) {
92
107
  const sourcePurl = sourceRef.toString();
93
108
  const targetPurl = targetRef.toString();
94
109
  // Ensure both components exist in the components list
95
110
  [sourceRef, targetRef].forEach((ref, index) => {
96
111
  const purl = index === 0 ? sourcePurl : targetPurl;
97
- if (this.getComponentIndex(purl) < 0) {
98
- this.components.push(getComponent(ref, "library", scope));
112
+ const existingIndex = this.getComponentIndex(purl);
113
+ if (existingIndex < 0) {
114
+ const hashes = index === 1 ? targetHashes : undefined;
115
+ this.components.push(getComponent(ref, "library", scope, undefined, hashes));
116
+ }
117
+ else if (index === 1 && targetHashes && targetHashes.length > 0 && !this.components[existingIndex].hashes) {
118
+ // Update hashes if the component was first seen without them (e.g. as a source)
119
+ this.components[existingIndex].hashes = targetHashes;
99
120
  }
100
121
  });
101
122
  // Ensure source dependency exists
@@ -13,6 +13,15 @@ export function selectTrustifyDABackend(opts?: {
13
13
  TRUSTIFY_DA_DEBUG?: string | undefined;
14
14
  TRUSTIFY_DA_BACKEND_URL?: string | undefined;
15
15
  }): string;
16
+ /**
17
+ * Generate a CycloneDX SBOM from a manifest file. No backend HTTP request is made.
18
+ *
19
+ * @param {string} manifestPath - path to the manifest file (e.g. pom.xml, package.json)
20
+ * @param {Options} [opts={}] - optional options (e.g. workspace dir, tool paths)
21
+ * @returns {Promise<object>} parsed CycloneDX SBOM JSON object
22
+ * @throws {Error} if the manifest is unsupported or SBOM generation fails
23
+ */
24
+ export function generateSbom(manifestPath: string, opts?: Options): Promise<object>;
16
25
  export { parseImageRef } from "./oci_image/utils.js";
17
26
  export { ImageRef } from "./oci_image/images.js";
18
27
  declare namespace _default {
@@ -21,6 +30,7 @@ declare namespace _default {
21
30
  export { stackAnalysisBatch };
22
31
  export { imageAnalysis };
23
32
  export { validateToken };
33
+ export { generateSbom };
24
34
  }
25
35
  export default _default;
26
36
  export type Options = {
@@ -64,11 +74,13 @@ export type Options = {
64
74
  TRUSTIFY_DA_CONTINUE_ON_ERROR?: string | undefined;
65
75
  batchMetadata?: boolean | undefined;
66
76
  TRUSTIFY_DA_BATCH_METADATA?: string | undefined;
77
+ TRUSTIFY_DA_UV_PATH?: string | undefined;
78
+ TRUSTIFY_DA_POETRY_PATH?: string | undefined;
67
79
  [key: string]: string | number | boolean | string[] | undefined;
68
80
  };
69
81
  export type BatchAnalysisMetadata = {
70
82
  workspaceRoot: string;
71
- ecosystem: "javascript" | "cargo" | "unknown";
83
+ ecosystem: "javascript" | "cargo" | "pyproject" | "unknown";
72
84
  total: number;
73
85
  successful: number;
74
86
  failed: number;
@@ -124,19 +136,74 @@ declare function stackAnalysis(manifest: string, html: false, opts?: Options | u
124
136
  * or backend request failed
125
137
  */
126
138
  declare function stackAnalysis(manifest: string, html?: boolean | undefined, opts?: Options | undefined): Promise<string | import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport").AnalysisReport>;
139
+ /**
140
+ * @overload
141
+ * @param {string} workspaceRoot
142
+ * @param {true} html
143
+ * @param {Options & { batchMetadata: true }} opts
144
+ * @returns {Promise<{ analysis: string, metadata: BatchAnalysisMetadata }>}
145
+ * @throws {Error}
146
+ */
147
+ declare function stackAnalysisBatch(workspaceRoot: string, html: true, opts: Options & {
148
+ batchMetadata: true;
149
+ }): Promise<{
150
+ analysis: string;
151
+ metadata: BatchAnalysisMetadata;
152
+ }>;
153
+ /**
154
+ * @overload
155
+ * @param {string} workspaceRoot
156
+ * @param {true} html
157
+ * @param {Options & { batchMetadata?: false }} [opts={}]
158
+ * @returns {Promise<string>}
159
+ * @throws {Error}
160
+ */
161
+ declare function stackAnalysisBatch(workspaceRoot: string, html: true, opts?: (Options & {
162
+ batchMetadata?: false;
163
+ }) | undefined): Promise<string>;
164
+ /**
165
+ * @overload
166
+ * @param {string} workspaceRoot
167
+ * @param {false} html
168
+ * @param {Options & { batchMetadata: true }} opts
169
+ * @returns {Promise<{ analysis: Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>, metadata: BatchAnalysisMetadata }>}
170
+ * @throws {Error}
171
+ */
172
+ declare function stackAnalysisBatch(workspaceRoot: string, html: false, opts: Options & {
173
+ batchMetadata: true;
174
+ }): Promise<{
175
+ analysis: {
176
+ [x: string]: import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport").AnalysisReport;
177
+ };
178
+ metadata: BatchAnalysisMetadata;
179
+ }>;
180
+ /**
181
+ * @overload
182
+ * @param {string} workspaceRoot
183
+ * @param {false} html
184
+ * @param {Options & { batchMetadata?: false }} [opts={}]
185
+ * @returns {Promise<Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>>}
186
+ * @throws {Error}
187
+ */
188
+ declare function stackAnalysisBatch(workspaceRoot: string, html: false, opts?: (Options & {
189
+ batchMetadata?: false;
190
+ }) | undefined): Promise<{
191
+ [x: string]: import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport").AnalysisReport;
192
+ }>;
127
193
  /**
128
194
  * Get stack analysis for all workspace packages/crates (batch).
129
195
  * Detects ecosystem from workspace root: Cargo (Cargo.toml + Cargo.lock) or JS/TS (package.json + lock file).
130
196
  * SBOMs are generated in parallel (see `batchConcurrency`) unless `continueOnError: false` (fail-fast sequential).
131
197
  * With `opts.batchMetadata` / `TRUSTIFY_DA_BATCH_METADATA`, returns `{ analysis, metadata }` including validation and SBOM errors.
132
198
  *
199
+ * @overload
133
200
  * @param {string} workspaceRoot - Path to workspace root (containing lock file and workspace config)
134
201
  * @param {boolean} [html=false] - true returns HTML, false returns JSON report
135
202
  * @param {Options} [opts={}] - `batchConcurrency`, discovery ignores, `continueOnError` (default true), `batchMetadata` (default false)
136
203
  * @returns {Promise<string|Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>|{ analysis: string|Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>, metadata: BatchAnalysisMetadata }>}
137
204
  * @throws {Error} if workspace root invalid, no manifests found, no packages pass validation, no SBOMs produced, or backend request failed. When `opts.batchMetadata` is set, `error.batchMetadata` may be set on thrown errors.
138
205
  */
139
- declare function stackAnalysisBatch(workspaceRoot: string, html?: boolean, opts?: Options): Promise<string | {
206
+ declare function stackAnalysisBatch(workspaceRoot: string, html?: boolean | undefined, opts?: Options | undefined): Promise<string | {
140
207
  [x: string]: import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport").AnalysisReport;
141
208
  } | {
142
209
  analysis: string | {
@@ -184,6 +251,10 @@ declare function imageAnalysis(imageRefs: Array<string>, html?: boolean | undefi
184
251
  * @throws {Error} if the backend request failed.
185
252
  */
186
253
  declare function validateToken(opts?: Options): Promise<object>;
254
+ import { discoverMavenModules } from './providers/java_maven.js';
255
+ import { discoverGradleSubprojects } from './providers/java_gradle.js';
256
+ import { discoverGoWorkspaceModules } from './providers/golang_gomodules.js';
257
+ import { discoverUvWorkspaceMembers } from './providers/python_uv.js';
187
258
  import { discoverWorkspacePackages } from './workspace.js';
188
259
  import { discoverWorkspaceCrates } from './workspace.js';
189
260
  import { validatePackageJson } from './workspace.js';
@@ -191,5 +262,5 @@ import { resolveWorkspaceDiscoveryIgnore } from './workspace.js';
191
262
  import { filterManifestPathsByDiscoveryIgnore } from './workspace.js';
192
263
  import { resolveContinueOnError } from './batch_opts.js';
193
264
  import { resolveBatchMetadata } from './batch_opts.js';
194
- export { discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata };
265
+ export { discoverMavenModules, discoverGradleSubprojects, discoverGoWorkspaceModules, discoverUvWorkspaceMembers, discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata };
195
266
  export { getProjectLicense, findLicenseFilePath, identifyLicense, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";
package/dist/src/index.js CHANGED
@@ -6,14 +6,18 @@ import analysis from './analysis.js';
6
6
  import fs from 'node:fs';
7
7
  import { getCustom } from "./tools.js";
8
8
  import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js';
9
+ import { discoverMavenModules } from './providers/java_maven.js';
10
+ import { discoverGradleSubprojects } from './providers/java_gradle.js';
11
+ import { discoverGoWorkspaceModules } from './providers/golang_gomodules.js';
12
+ import { discoverUvWorkspaceMembers } from './providers/python_uv.js';
9
13
  import { discoverWorkspaceCrates, discoverWorkspacePackages, filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore, validatePackageJson, } from './workspace.js';
10
14
  import.meta.dirname;
11
15
  import * as url from 'url';
12
16
  export { parseImageRef } from "./oci_image/utils.js";
13
17
  export { ImageRef } from "./oci_image/images.js";
14
18
  export { getProjectLicense, findLicenseFilePath, identifyLicense, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";
15
- export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken };
16
- export { discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata, };
19
+ export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken, generateSbom };
20
+ export { discoverMavenModules, discoverGradleSubprojects, discoverGoWorkspaceModules, discoverUvWorkspaceMembers, discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata, };
17
21
  /**
18
22
  * @typedef {{
19
23
  * TRUSTIFY_DA_CARGO_PATH?: string | undefined,
@@ -56,13 +60,15 @@ export { discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson
56
60
  * TRUSTIFY_DA_CONTINUE_ON_ERROR?: string | undefined,
57
61
  * batchMetadata?: boolean | undefined,
58
62
  * TRUSTIFY_DA_BATCH_METADATA?: string | undefined,
63
+ * TRUSTIFY_DA_UV_PATH?: string | undefined,
64
+ * TRUSTIFY_DA_POETRY_PATH?: string | undefined,
59
65
  * [key: string]: string | number | boolean | string[] | undefined,
60
66
  * }} Options
61
67
  */
62
68
  /**
63
69
  * @typedef {{
64
70
  * workspaceRoot: string,
65
- * ecosystem: 'javascript' | 'cargo' | 'unknown',
71
+ * ecosystem: 'javascript' | 'cargo' | 'pyproject' | 'unknown',
66
72
  * total: number,
67
73
  * successful: number,
68
74
  * failed: number,
@@ -169,7 +175,9 @@ async function componentAnalysis(manifest, opts = {}) {
169
175
  fs.accessSync(manifest, fs.constants.R_OK);
170
176
  opts["manifest-type"] = path.basename(manifest);
171
177
  let provider = match(manifest, availableProviders, opts); // throws error if no matching provider
172
- return await analysis.requestComponent(provider, manifest, theUrl, opts); // throws error request sending failed
178
+ const result = await analysis.requestComponent(provider, manifest, theUrl, opts); // throws error request sending failed
179
+ result.packageManager = provider.packageManagerName();
180
+ return result;
173
181
  }
174
182
  /**
175
183
  * @overload
@@ -235,6 +243,22 @@ function buildBatchAnalysisMetadata(root, ecosystem, totalSbomAttempts, successf
235
243
  errors: [...errors],
236
244
  };
237
245
  }
246
+ /**
247
+ * Generate a CycloneDX SBOM from a manifest file. No backend HTTP request is made.
248
+ *
249
+ * @param {string} manifestPath - path to the manifest file (e.g. pom.xml, package.json)
250
+ * @param {Options} [opts={}] - optional options (e.g. workspace dir, tool paths)
251
+ * @returns {Promise<object>} parsed CycloneDX SBOM JSON object
252
+ * @throws {Error} if the manifest is unsupported or SBOM generation fails
253
+ */
254
+ export async function generateSbom(manifestPath, opts = {}) {
255
+ fs.accessSync(manifestPath, fs.constants.R_OK);
256
+ const result = await generateOneSbom(manifestPath, opts);
257
+ if (!result.ok) {
258
+ throw new Error(`Failed to generate SBOM for ${result.manifestPath}: ${result.reason}`);
259
+ }
260
+ return result.sbom;
261
+ }
238
262
  /**
239
263
  * @typedef {{ ok: true, purl: string, sbom: object } | { ok: false, manifestPath: string, reason: string }} SbomResult
240
264
  */
@@ -261,17 +285,45 @@ async function generateOneSbom(manifestPath, workspaceOpts) {
261
285
  *
262
286
  * @param {string} root - Resolved workspace root
263
287
  * @param {Options} opts
264
- * @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'unknown', manifestPaths: string[] }>}
288
+ * @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'maven' | 'gradle' | 'gomodules' | 'pyproject' | 'unknown', manifestPaths: string[] }>}
265
289
  * @private
266
290
  */
267
291
  async function detectWorkspaceManifests(root, opts) {
268
292
  const cargoToml = path.join(root, 'Cargo.toml');
269
293
  const cargoLock = path.join(root, 'Cargo.lock');
270
294
  const packageJson = path.join(root, 'package.json');
295
+ const pomXml = path.join(root, 'pom.xml');
271
296
  if (fs.existsSync(cargoToml) && fs.existsSync(cargoLock)) {
272
297
  return { ecosystem: 'cargo', manifestPaths: await discoverWorkspaceCrates(root, opts) };
273
298
  }
274
- const hasJsLock = fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
299
+ if (fs.existsSync(pomXml)) {
300
+ const manifestPaths = await discoverMavenModules(root, opts);
301
+ if (manifestPaths.length > 0) {
302
+ return { ecosystem: 'maven', manifestPaths };
303
+ }
304
+ }
305
+ const hasGradleSettings = fs.existsSync(path.join(root, 'settings.gradle'))
306
+ || fs.existsSync(path.join(root, 'settings.gradle.kts'));
307
+ if (hasGradleSettings) {
308
+ const manifestPaths = await discoverGradleSubprojects(root, opts);
309
+ if (manifestPaths.length > 0) {
310
+ return { ecosystem: 'gradle', manifestPaths };
311
+ }
312
+ }
313
+ if (fs.existsSync(path.join(root, 'go.work'))) {
314
+ const manifestPaths = await discoverGoWorkspaceModules(root, opts);
315
+ if (manifestPaths.length > 0) {
316
+ return { ecosystem: 'gomodules', manifestPaths };
317
+ }
318
+ }
319
+ if (fs.existsSync(path.join(root, 'pyproject.toml')) && fs.existsSync(path.join(root, 'uv.lock'))) {
320
+ const manifestPaths = await discoverUvWorkspaceMembers(root, opts);
321
+ if (manifestPaths.length > 0) {
322
+ return { ecosystem: 'pyproject', manifestPaths };
323
+ }
324
+ }
325
+ const hasJsLock = fs.existsSync(path.join(root, 'bun.lock'))
326
+ || fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
275
327
  || fs.existsSync(path.join(root, 'yarn.lock'))
276
328
  || fs.existsSync(path.join(root, 'package-lock.json'));
277
329
  if (fs.existsSync(packageJson) && hasJsLock) {
@@ -380,12 +432,45 @@ function batchError(message, wantMetadata, metadata) {
380
432
  }
381
433
  return err;
382
434
  }
435
+ /**
436
+ * @overload
437
+ * @param {string} workspaceRoot
438
+ * @param {true} html
439
+ * @param {Options & { batchMetadata: true }} opts
440
+ * @returns {Promise<{ analysis: string, metadata: BatchAnalysisMetadata }>}
441
+ * @throws {Error}
442
+ */
443
+ /**
444
+ * @overload
445
+ * @param {string} workspaceRoot
446
+ * @param {true} html
447
+ * @param {Options & { batchMetadata?: false }} [opts={}]
448
+ * @returns {Promise<string>}
449
+ * @throws {Error}
450
+ */
451
+ /**
452
+ * @overload
453
+ * @param {string} workspaceRoot
454
+ * @param {false} html
455
+ * @param {Options & { batchMetadata: true }} opts
456
+ * @returns {Promise<{ analysis: Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>, metadata: BatchAnalysisMetadata }>}
457
+ * @throws {Error}
458
+ */
459
+ /**
460
+ * @overload
461
+ * @param {string} workspaceRoot
462
+ * @param {false} html
463
+ * @param {Options & { batchMetadata?: false }} [opts={}]
464
+ * @returns {Promise<Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>>}
465
+ * @throws {Error}
466
+ */
383
467
  /**
384
468
  * Get stack analysis for all workspace packages/crates (batch).
385
469
  * Detects ecosystem from workspace root: Cargo (Cargo.toml + Cargo.lock) or JS/TS (package.json + lock file).
386
470
  * SBOMs are generated in parallel (see `batchConcurrency`) unless `continueOnError: false` (fail-fast sequential).
387
471
  * With `opts.batchMetadata` / `TRUSTIFY_DA_BATCH_METADATA`, returns `{ analysis, metadata }` including validation and SBOM errors.
388
472
  *
473
+ * @overload
389
474
  * @param {string} workspaceRoot - Path to workspace root (containing lock file and workspace config)
390
475
  * @param {boolean} [html=false] - true returns HTML, false returns JSON report
391
476
  * @param {Options} [opts={}] - `batchConcurrency`, discovery ignores, `continueOnError` (default true), `batchMetadata` (default false)
@@ -416,7 +501,7 @@ async function stackAnalysisBatch(workspaceRoot, html = false, opts = {}) {
416
501
  }
417
502
  }
418
503
  if (manifestPaths.length === 0) {
419
- throw new Error(`No workspace manifests found at ${root}. Ensure Cargo.toml+Cargo.lock or package.json+lock file exist.`);
504
+ throw new Error(`No workspace manifests found at ${root}. Ensure a supported workspace root exists (Cargo.toml+Cargo.lock, pom.xml, build.gradle, go.work, pyproject.toml+uv.lock, or package.json+lock file).`);
420
505
  }
421
506
  const workspaceOpts = { ...opts, TRUSTIFY_DA_WORKSPACE_DIR: root };
422
507
  const concurrency = resolveBatchConcurrency(opts);
@@ -135,7 +135,7 @@ function execSyft(imageRef, opts = {}) {
135
135
  function getSyftEnvs(dockerPath, podmanPath) {
136
136
  let path = null;
137
137
  if (dockerPath && podmanPath) {
138
- path = `${dockerPath}${File.pathSeparator}${podmanPath}`;
138
+ path = `${dockerPath}${delimiter}${podmanPath}`;
139
139
  }
140
140
  else if (dockerPath) {
141
141
  path = dockerPath;
@@ -276,7 +276,16 @@ function podmanGetVariant(opts = {}) {
276
276
  * @returns {string} - The information
277
277
  */
278
278
  function dockerPodmanInfo(dockerSupplier, podmanSupplier, opts = {}) {
279
- return dockerSupplier(opts) || podmanSupplier(opts);
279
+ try {
280
+ const result = dockerSupplier(opts);
281
+ if (result) {
282
+ return result;
283
+ }
284
+ }
285
+ catch (_) {
286
+ // docker not available, fall through to podman
287
+ }
288
+ return podmanSupplier(opts);
280
289
  }
281
290
  /**
282
291
  * Gets the digests for an image
@@ -21,7 +21,7 @@ export function match(manifest: string, providers: [Provider], opts?: {
21
21
  TRUSTIFY_DA_WORKSPACE_DIR?: string;
22
22
  }): Provider;
23
23
  /** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
24
- /** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string, Object): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>, readLicenseFromManifest: function(string): string | null}} Provider */
24
+ /** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string, Object): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>, readLicenseFromManifest: function(string, Object=): string | null, packageManagerName: function(): string}} Provider */
25
25
  /**
26
26
  * MUST include all providers here.
27
27
  * @type {[Provider]}
@@ -37,5 +37,6 @@ export type Provider = {
37
37
  validateLockFile: (arg0: string, arg1: any) => void;
38
38
  provideComponent: (arg0: string, arg1: {}) => Provided | Promise<Provided>;
39
39
  provideStack: (arg0: string, arg1: {}) => Provided | Promise<Provided>;
40
- readLicenseFromManifest: (arg0: string) => string | null;
40
+ readLicenseFromManifest: (arg0: string, arg1: any | undefined) => string | null;
41
+ packageManagerName: () => string;
41
42
  };