@trustify-da/trustify-da-javascript-client 0.3.0-ea.61444d4 → 0.3.0-ea.62b88e5

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 (59) hide show
  1. package/README.md +22 -8
  2. package/dist/package.json +6 -5
  3. package/dist/src/analysis.d.ts +40 -19
  4. package/dist/src/analysis.js +41 -5
  5. package/dist/src/cli.js +95 -17
  6. package/dist/src/config.d.ts +120 -0
  7. package/dist/src/config.js +260 -0
  8. package/dist/src/cyclone_dx_sbom.d.ts +13 -0
  9. package/dist/src/cyclone_dx_sbom.js +39 -1
  10. package/dist/src/index.d.ts +23 -23
  11. package/dist/src/index.js +23 -39
  12. package/dist/src/license/index.d.ts +2 -2
  13. package/dist/src/license/index.js +9 -6
  14. package/dist/src/license/licenses_api.d.ts +2 -2
  15. package/dist/src/license/licenses_api.js +1 -1
  16. package/dist/src/package_version.d.ts +8 -0
  17. package/dist/src/package_version.js +31 -0
  18. package/dist/src/providers/base_java.d.ts +53 -0
  19. package/dist/src/providers/base_java.js +64 -17
  20. package/dist/src/providers/base_javascript.d.ts +54 -0
  21. package/dist/src/providers/base_javascript.js +106 -4
  22. package/dist/src/providers/golang_gomodules.js +6 -5
  23. package/dist/src/providers/java_gradle.d.ts +48 -0
  24. package/dist/src/providers/java_gradle.js +204 -20
  25. package/dist/src/providers/java_maven.d.ts +28 -8
  26. package/dist/src/providers/java_maven.js +104 -10
  27. package/dist/src/providers/javascript_bun.d.ts +12 -0
  28. package/dist/src/providers/javascript_bun.js +42 -1
  29. package/dist/src/providers/javascript_npm.d.ts +13 -0
  30. package/dist/src/providers/javascript_npm.js +40 -1
  31. package/dist/src/providers/javascript_pnpm.d.ts +13 -0
  32. package/dist/src/providers/javascript_pnpm.js +47 -1
  33. package/dist/src/providers/javascript_yarn.d.ts +12 -0
  34. package/dist/src/providers/javascript_yarn.js +75 -2
  35. package/dist/src/providers/manifest.js +6 -3
  36. package/dist/src/providers/processors/yarn_berry_processor.d.ts +5 -1
  37. package/dist/src/providers/processors/yarn_berry_processor.js +3 -2
  38. package/dist/src/providers/processors/yarn_classic_processor.d.ts +5 -1
  39. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  40. package/dist/src/providers/python_uv.js +6 -0
  41. package/dist/src/providers/requirements_parser.js +1 -1
  42. package/dist/src/providers/rust_cargo.js +34 -52
  43. package/dist/src/remediate.d.ts +90 -4
  44. package/dist/src/remediate.js +153 -58
  45. package/dist/src/remediation.d.ts +54 -30
  46. package/dist/src/remediation.js +92 -53
  47. package/dist/src/remediation_report.d.ts +3 -20
  48. package/dist/src/remediation_report.js +37 -16
  49. package/dist/src/sbom.d.ts +11 -0
  50. package/dist/src/sbom.js +10 -0
  51. package/dist/src/tools.d.ts +9 -11
  52. package/dist/src/tools.js +34 -13
  53. package/dist/src/updaters/maven_updater.d.ts +40 -8
  54. package/dist/src/updaters/maven_updater.js +26 -3
  55. package/dist/src/updaters/toml_updater.d.ts +42 -13
  56. package/dist/src/updaters/toml_updater.js +36 -4
  57. package/dist/src/workspace.d.ts +2 -1
  58. package/dist/src/workspace.js +2 -1
  59. package/package.json +7 -6
@@ -1,11 +1,51 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { parse } from 'jsonc-parser';
4
- import Base_javascript from './base_javascript.js';
4
+ import Base_javascript, { sriToHash } from './base_javascript.js';
5
5
  export default class Javascript_bun extends Base_javascript {
6
6
  _lockFileName() {
7
7
  return "bun.lock";
8
8
  }
9
+ /**
10
+ * Parses `bun.lock` into a hash map keyed by `name@version`. Each entry in
11
+ * the `packages` section is an array whose first element is the resolved id
12
+ * (`name@version`) and whose fourth element is the SRI integrity string.
13
+ * @param {string} lockDir - Directory containing bun.lock
14
+ * @returns {Map<string, Array<{alg: string, content: string}>>} Hash map
15
+ * @protected
16
+ */
17
+ _parseLockFileHashes(lockDir) {
18
+ const map = new Map();
19
+ const lockPath = path.join(lockDir, this._lockFileName());
20
+ if (!fs.existsSync(lockPath)) {
21
+ return map;
22
+ }
23
+ let lockData;
24
+ try {
25
+ lockData = parse(fs.readFileSync(lockPath, 'utf-8'));
26
+ }
27
+ catch (_) {
28
+ return map;
29
+ }
30
+ const packages = lockData?.packages || {};
31
+ for (const entry of Object.values(packages)) {
32
+ if (!Array.isArray(entry) || typeof entry[0] !== 'string' || typeof entry[3] !== 'string') {
33
+ continue;
34
+ }
35
+ const id = entry[0];
36
+ const at = id.lastIndexOf('@');
37
+ if (at <= 0) {
38
+ continue;
39
+ }
40
+ const name = id.slice(0, at);
41
+ const version = id.slice(at + 1);
42
+ const hash = sriToHash(entry[3]);
43
+ if (name && version && hash) {
44
+ map.set(`${name}@${version}`, [hash]);
45
+ }
46
+ }
47
+ return map;
48
+ }
9
49
  _cmdName() {
10
50
  return "bun";
11
51
  }
@@ -20,6 +60,7 @@ export default class Javascript_bun extends Base_javascript {
20
60
  const manifestDir = path.dirname(this._getManifest().manifestPath);
21
61
  const lockDir = this._findLockFileDir(manifestDir, opts) || manifestDir;
22
62
  this._createLockFile(lockDir);
63
+ this._loadHashes(lockDir);
23
64
  const lockContent = fs.readFileSync(path.join(lockDir, 'bun.lock'), 'utf-8');
24
65
  const lockData = parse(lockContent);
25
66
  const packages = lockData.packages || {};
@@ -1,4 +1,17 @@
1
1
  export default class Javascript_npm extends Base_javascript {
2
+ /**
3
+ * Parses `package-lock.json` (lockfileVersion 2/3) into a hash map keyed by
4
+ * `name@version`. Each `packages` entry is keyed by its install path
5
+ * (e.g. `node_modules/@hapi/joi`); the package name is the segment after the
6
+ * final `node_modules/`. The `integrity` field holds an SRI string.
7
+ * @param {string} lockDir - Directory containing package-lock.json
8
+ * @returns {Map<string, Array<{alg: string, content: string}>>} Hash map
9
+ * @protected
10
+ */
11
+ protected _parseLockFileHashes(lockDir: string): Map<string, Array<{
12
+ alg: string;
13
+ content: string;
14
+ }>>;
2
15
  _listCmdArgs(includeTransitive: any): string[];
3
16
  _buildDependencyTree(includeTransitive: any, opts?: {}): Object | {
4
17
  name: any;
@@ -1,8 +1,47 @@
1
- import Base_javascript from './base_javascript.js';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import Base_javascript, { sriToHash } from './base_javascript.js';
2
4
  export default class Javascript_npm extends Base_javascript {
3
5
  _lockFileName() {
4
6
  return "package-lock.json";
5
7
  }
8
+ /**
9
+ * Parses `package-lock.json` (lockfileVersion 2/3) into a hash map keyed by
10
+ * `name@version`. Each `packages` entry is keyed by its install path
11
+ * (e.g. `node_modules/@hapi/joi`); the package name is the segment after the
12
+ * final `node_modules/`. The `integrity` field holds an SRI string.
13
+ * @param {string} lockDir - Directory containing package-lock.json
14
+ * @returns {Map<string, Array<{alg: string, content: string}>>} Hash map
15
+ * @protected
16
+ */
17
+ _parseLockFileHashes(lockDir) {
18
+ const map = new Map();
19
+ const lockPath = path.join(lockDir, this._lockFileName());
20
+ if (!fs.existsSync(lockPath)) {
21
+ return map;
22
+ }
23
+ let lock;
24
+ try {
25
+ lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
26
+ }
27
+ catch (_) {
28
+ return map;
29
+ }
30
+ const packages = lock.packages || {};
31
+ for (const [pkgPath, entry] of Object.entries(packages)) {
32
+ if (!pkgPath || !entry || !entry.integrity || !entry.version) {
33
+ continue;
34
+ }
35
+ const marker = 'node_modules/';
36
+ const idx = pkgPath.lastIndexOf(marker);
37
+ const name = idx < 0 ? pkgPath : pkgPath.slice(idx + marker.length);
38
+ const hash = sriToHash(entry.integrity);
39
+ if (name && hash) {
40
+ map.set(`${name}@${entry.version}`, [hash]);
41
+ }
42
+ }
43
+ return map;
44
+ }
6
45
  _cmdName() {
7
46
  return "npm";
8
47
  }
@@ -1,4 +1,17 @@
1
1
  export default class Javascript_pnpm extends Base_javascript {
2
+ /**
3
+ * Parses `pnpm-lock.yaml` (lockfileVersion 9) into a hash map keyed by
4
+ * `name@version`. Entries in the `packages` section are keyed by
5
+ * `<name>@<version>` (optionally followed by a `(peer@x)` suffix); the
6
+ * `resolution.integrity` field holds an SRI string.
7
+ * @param {string} lockDir - Directory containing pnpm-lock.yaml
8
+ * @returns {Map<string, Array<{alg: string, content: string}>>} Hash map
9
+ * @protected
10
+ */
11
+ protected _parseLockFileHashes(lockDir: string): Map<string, Array<{
12
+ alg: string;
13
+ content: string;
14
+ }>>;
2
15
  _listCmdArgs(includeTransitive: any): string[];
3
16
  _buildDependencyTree(includeTransitive: any, opts?: {}): any;
4
17
  }
@@ -1,8 +1,54 @@
1
- import Base_javascript from './base_javascript.js';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { load as yamlLoad } from 'js-yaml';
4
+ import Base_javascript, { sriToHash } from './base_javascript.js';
2
5
  export default class Javascript_pnpm extends Base_javascript {
3
6
  _lockFileName() {
4
7
  return "pnpm-lock.yaml";
5
8
  }
9
+ /**
10
+ * Parses `pnpm-lock.yaml` (lockfileVersion 9) into a hash map keyed by
11
+ * `name@version`. Entries in the `packages` section are keyed by
12
+ * `<name>@<version>` (optionally followed by a `(peer@x)` suffix); the
13
+ * `resolution.integrity` field holds an SRI string.
14
+ * @param {string} lockDir - Directory containing pnpm-lock.yaml
15
+ * @returns {Map<string, Array<{alg: string, content: string}>>} Hash map
16
+ * @protected
17
+ */
18
+ _parseLockFileHashes(lockDir) {
19
+ const map = new Map();
20
+ const lockPath = path.join(lockDir, this._lockFileName());
21
+ if (!fs.existsSync(lockPath)) {
22
+ return map;
23
+ }
24
+ let lock;
25
+ try {
26
+ lock = yamlLoad(fs.readFileSync(lockPath, 'utf-8'));
27
+ }
28
+ catch (_) {
29
+ return map;
30
+ }
31
+ const packages = lock?.packages || {};
32
+ for (const [key, entry] of Object.entries(packages)) {
33
+ const integrity = entry?.resolution?.integrity;
34
+ if (!key || !integrity) {
35
+ continue;
36
+ }
37
+ // Strip any peer-dependency suffix, then split name from version
38
+ const base = key.split('(')[0];
39
+ const at = base.lastIndexOf('@');
40
+ if (at <= 0) {
41
+ continue;
42
+ }
43
+ const name = base.slice(0, at);
44
+ const version = base.slice(at + 1);
45
+ const hash = sriToHash(integrity);
46
+ if (name && version && hash) {
47
+ map.set(`${name}@${version}`, [hash]);
48
+ }
49
+ }
50
+ return map;
51
+ }
6
52
  _cmdName() {
7
53
  return "pnpm";
8
54
  }
@@ -1,5 +1,17 @@
1
1
  export default class Javascript_yarn extends Base_javascript {
2
2
  static VERSION_PATTERN: RegExp;
3
+ /**
4
+ * Parses `yarn.lock` into a hash map keyed by `name@version`. Supports both
5
+ * Yarn Classic (v1) entries with an `integrity` SRI field and Yarn Berry
6
+ * (v2+) entries with a `checksum` field.
7
+ * @param {string} lockDir - Directory containing yarn.lock
8
+ * @returns {Map<string, Array<{alg: string, content: string}>>} Hash map
9
+ * @protected
10
+ */
11
+ protected _parseLockFileHashes(lockDir: string): Map<string, Array<{
12
+ alg: string;
13
+ content: string;
14
+ }>>;
3
15
  _listCmdArgs(includeTransitive: any, manifestDir: any): any;
4
16
  _updateLockFileCmdArgs(manifestDir: any): any;
5
17
  _setUp(manifestPath: any, opts: any): void;
@@ -1,12 +1,85 @@
1
- import Base_javascript from './base_javascript.js';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseSyml } from '@yarnpkg/parsers';
4
+ import Base_javascript, { sriToHash } from './base_javascript.js';
2
5
  import Yarn_berry_processor from './processors/yarn_berry_processor.js';
3
6
  import Yarn_classic_processor from './processors/yarn_classic_processor.js';
7
+ /**
8
+ * Converts a Yarn Berry `checksum` value to a CycloneDX hash object. Berry
9
+ * checksums are formatted as `<cacheKey>/<hex-sha512>`; the digest is the
10
+ * hex-encoded SHA-512 following the final slash.
11
+ * @param {string} checksum - The checksum field value
12
+ * @returns {{alg: string, content: string}|null} CycloneDX hash, or null
13
+ */
14
+ function berryChecksumToHash(checksum) {
15
+ if (typeof checksum !== 'string') {
16
+ return null;
17
+ }
18
+ const slash = checksum.lastIndexOf('/');
19
+ const hex = (slash >= 0 ? checksum.slice(slash + 1) : checksum).trim();
20
+ if (!/^[0-9a-f]+$/i.test(hex)) {
21
+ return null;
22
+ }
23
+ return { alg: 'SHA-512', content: hex.toLowerCase() };
24
+ }
4
25
  export default class Javascript_yarn extends Base_javascript {
5
26
  static VERSION_PATTERN = /^([0-9]+)\./;
6
27
  #processor;
7
28
  _lockFileName() {
8
29
  return "yarn.lock";
9
30
  }
31
+ /**
32
+ * Parses `yarn.lock` into a hash map keyed by `name@version`. Supports both
33
+ * Yarn Classic (v1) entries with an `integrity` SRI field and Yarn Berry
34
+ * (v2+) entries with a `checksum` field.
35
+ * @param {string} lockDir - Directory containing yarn.lock
36
+ * @returns {Map<string, Array<{alg: string, content: string}>>} Hash map
37
+ * @protected
38
+ */
39
+ _parseLockFileHashes(lockDir) {
40
+ const map = new Map();
41
+ const lockPath = path.join(lockDir, this._lockFileName());
42
+ if (!fs.existsSync(lockPath)) {
43
+ return map;
44
+ }
45
+ let content;
46
+ try {
47
+ content = fs.readFileSync(lockPath, 'utf-8');
48
+ }
49
+ catch (_) {
50
+ return map;
51
+ }
52
+ const parsed = parseSyml(content);
53
+ for (const [spec, entry] of Object.entries(parsed)) {
54
+ if (spec === '__metadata') {
55
+ continue;
56
+ }
57
+ const version = entry.version;
58
+ if (!version) {
59
+ continue;
60
+ }
61
+ let hash = null;
62
+ if (entry.integrity) {
63
+ hash = sriToHash(entry.integrity);
64
+ }
65
+ else if (entry.checksum) {
66
+ hash = berryChecksumToHash(entry.checksum);
67
+ }
68
+ if (!hash) {
69
+ continue;
70
+ }
71
+ // Handle comma-separated specifiers (e.g., "pkg@^1.0.0, pkg@^2.0.0")
72
+ for (const rawSpec of spec.split(',')) {
73
+ const trimmed = rawSpec.trim();
74
+ const atIndex = trimmed.lastIndexOf('@');
75
+ if (atIndex > 0) {
76
+ const name = trimmed.slice(0, atIndex);
77
+ map.set(`${name}@${version}`, [hash]);
78
+ }
79
+ }
80
+ }
81
+ return map;
82
+ }
10
83
  _cmdName() {
11
84
  return "yarn";
12
85
  }
@@ -34,6 +107,6 @@ export default class Javascript_yarn extends Base_javascript {
34
107
  return this.#processor.parseDepTreeOutput(output);
35
108
  }
36
109
  _addDependenciesToSbom(sbom, depTree) {
37
- this.#processor.addDependenciesToSbom(sbom, depTree);
110
+ this.#processor.addDependenciesToSbom(sbom, depTree, purl => this._hashesForPurl(purl));
38
111
  }
39
112
  }
@@ -55,11 +55,14 @@ export default class Manifest {
55
55
  }
56
56
  loadIgnored(content) {
57
57
  let deps = [];
58
- if (!content.exhortignore) {
58
+ // Prefer the new 'trustify-da-ignore' key, falling back to the legacy
59
+ // 'exhortignore' key for backwards compatibility.
60
+ let ignored = content['trustify-da-ignore'] || content.exhortignore;
61
+ if (!ignored) {
59
62
  return deps;
60
63
  }
61
- for (let i = 0; i < content.exhortignore.length; i++) {
62
- deps.push(toPurl("npm", content.exhortignore[i]));
64
+ for (let i = 0; i < ignored.length; i++) {
65
+ deps.push(toPurl("npm", ignored[i]));
63
66
  }
64
67
  return deps;
65
68
  }
@@ -34,8 +34,12 @@ export default class Yarn_berry_processor extends Yarn_processor {
34
34
  * Adds dependencies to the SBOM
35
35
  * @param {Sbom} sbom - The SBOM object to add dependencies to
36
36
  * @param {Object} depTree - The dependency tree object
37
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
37
38
  */
38
- addDependenciesToSbom(sbom: Sbom, depTree: Object): void;
39
+ addDependenciesToSbom(sbom: Sbom, depTree: Object, hashesForPurl?: (arg0: PackageURL) => (Array<{
40
+ alg: string;
41
+ content: string;
42
+ }> | undefined)): void;
39
43
  #private;
40
44
  }
41
45
  import Yarn_processor from "./yarn_processor.js";
@@ -78,8 +78,9 @@ export default class Yarn_berry_processor extends Yarn_processor {
78
78
  * Adds dependencies to the SBOM
79
79
  * @param {Sbom} sbom - The SBOM object to add dependencies to
80
80
  * @param {Object} depTree - The dependency tree object
81
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
81
82
  */
82
- addDependenciesToSbom(sbom, depTree) {
83
+ addDependenciesToSbom(sbom, depTree, hashesForPurl = () => undefined) {
83
84
  if (!depTree) {
84
85
  return;
85
86
  }
@@ -137,7 +138,7 @@ export default class Yarn_berry_processor extends Yarn_processor {
137
138
  }
138
139
  const to = this.#purlFromLocator(d.locator);
139
140
  if (to) {
140
- sbom.addDependency(from, to);
141
+ sbom.addDependency(from, to, undefined, hashesForPurl(to));
141
142
  }
142
143
  });
143
144
  });
@@ -30,8 +30,12 @@ export default class Yarn_classic_processor extends Yarn_processor {
30
30
  * Adds dependencies to the SBOM
31
31
  * @param {Sbom} sbom - The SBOM object to add dependencies to
32
32
  * @param {Object} depTree - The dependency tree object
33
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
33
34
  */
34
- addDependenciesToSbom(sbom: Sbom, depTree: Object): void;
35
+ addDependenciesToSbom(sbom: Sbom, depTree: Object, hashesForPurl?: (arg0: PackageURL) => (Array<{
36
+ alg: string;
37
+ content: string;
38
+ }> | undefined)): void;
35
39
  #private;
36
40
  }
37
41
  import Yarn_processor from "./yarn_processor.js";
@@ -50,8 +50,9 @@ export default class Yarn_classic_processor extends Yarn_processor {
50
50
  * Adds dependencies to the SBOM
51
51
  * @param {Sbom} sbom - The SBOM object to add dependencies to
52
52
  * @param {Object} depTree - The dependency tree object
53
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} [hashesForPurl] - Resolves lock-file hashes for a purl
53
54
  */
54
- addDependenciesToSbom(sbom, depTree) {
55
+ addDependenciesToSbom(sbom, depTree, hashesForPurl = () => undefined) {
55
56
  if (!depTree?.data?.trees) {
56
57
  return;
57
58
  }
@@ -60,12 +61,12 @@ export default class Yarn_classic_processor extends Yarn_processor {
60
61
  depTree.data.trees.forEach(n => {
61
62
  const dep = new NodeMetaData(n);
62
63
  if (this._manifest.dependencies.includes(dep.name)) {
63
- sbom.addDependency(rootPurl, dep.purl);
64
+ sbom.addDependency(rootPurl, dep.purl, undefined, hashesForPurl(dep.purl));
64
65
  }
65
66
  purls.set(dep.name, dep.purl);
66
67
  });
67
68
  depTree.data.trees.forEach(n => {
68
- this.#addChildrenToSbom(sbom, n, purls);
69
+ this.#addChildrenToSbom(sbom, n, purls, hashesForPurl);
69
70
  });
70
71
  }
71
72
  /**
@@ -73,9 +74,10 @@ export default class Yarn_classic_processor extends Yarn_processor {
73
74
  * @param {Sbom} sbom - The SBOM object to add dependencies to
74
75
  * @param {Object} node - The current dependency node
75
76
  * @param {Map<string, PackageURL>} purls - Map of dependency names to their PackageURL objects
77
+ * @param {function(PackageURL): (Array<{alg: string, content: string}>|undefined)} hashesForPurl - Resolves lock-file hashes for a purl
76
78
  * @private
77
79
  */
78
- #addChildrenToSbom(sbom, node, purls) {
80
+ #addChildrenToSbom(sbom, node, purls, hashesForPurl) {
79
81
  const dep = new NodeMetaData(node);
80
82
  const children = node.children ? node.children : [];
81
83
  children.forEach(c => {
@@ -83,9 +85,9 @@ export default class Yarn_classic_processor extends Yarn_processor {
83
85
  const from = dep.shadow ? purls.get(dep.name) : dep.purl;
84
86
  const to = child.shadow ? purls.get(child.name) : child.purl;
85
87
  if (from && to) {
86
- sbom.addDependency(from, to);
88
+ sbom.addDependency(from, to, undefined, hashesForPurl(to));
87
89
  }
88
- this.#addChildrenToSbom(sbom, c, purls);
90
+ this.#addChildrenToSbom(sbom, c, purls, hashesForPurl);
89
91
  });
90
92
  }
91
93
  }
@@ -144,6 +144,12 @@ export default class Python_uv extends Base_pyproject {
144
144
  continue;
145
145
  }
146
146
  }
147
+ // PEP 440 direct references (name @ url) — skip, no pinned version available
148
+ if (child.children.find(c => c.type === 'url_spec')) {
149
+ currentPkg = null;
150
+ collectingVia = false;
151
+ continue;
152
+ }
147
153
  let name = nameNode.text;
148
154
  let version = null;
149
155
  let versionMatches = pinnedVersionQuery.matches(child);
@@ -16,7 +16,7 @@ export async function getRequirementQuery() {
16
16
  }
17
17
  export async function getIgnoreQuery() {
18
18
  const language = await init();
19
- return new Query(language, '((requirement (package) @name) @req . (comment) @comment (#match? @comment "^#[\\t ]*exhortignore"))');
19
+ return new Query(language, '((requirement (package) @name) @req . (comment) @comment (#match? @comment "^#[\\t ]*(exhortignore|trustify-da-ignore)(?![\\w-])"))');
20
20
  }
21
21
  export async function getPinnedVersionQuery() {
22
22
  const language = await init();
@@ -116,30 +116,25 @@ function resolveWorkspaceLicense(parsed, metadata) {
116
116
  return parsed.workspace?.package?.license || null;
117
117
  }
118
118
  /**
119
- * Validates that Cargo.lock exists in the manifest directory or in a parent
120
- * workspace root directory. In Cargo workspaces the lock file always lives at
121
- * the workspace root, so when a member crate's Cargo.toml is provided we walk
122
- * up the directory tree looking for Cargo.lock (stopping when we find a
123
- * Cargo.toml that contains a [workspace] section, or when we reach the
124
- * filesystem root).
125
- * When TRUSTIFY_DA_WORKSPACE_DIR is provided (via env var or opts),
126
- * checks only that directory for Cargo.lock — no walk-up.
119
+ * Finds the directory containing Cargo.lock, honoring TRUSTIFY_DA_WORKSPACE_DIR.
120
+ * Walks up from manifestDir looking for Cargo.lock, stopping at workspace root or filesystem root.
127
121
  * @param {string} manifestDir - the directory where the manifest lies
128
122
  * @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
129
- * @returns {boolean} true if Cargo.lock is found
123
+ * @returns {string|null} the directory path containing Cargo.lock, or null if not found
124
+ * @private
130
125
  */
131
- function validateLockFile(manifestDir, opts = {}) {
126
+ function _findCargoLockDir(manifestDir, opts = {}) {
132
127
  const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts);
133
128
  if (workspaceDir) {
134
129
  const dir = path.resolve(workspaceDir);
135
- return fs.existsSync(path.join(dir, 'Cargo.lock'));
130
+ return fs.existsSync(path.join(dir, 'Cargo.lock')) ? dir : null;
136
131
  }
137
132
  let dir = path.resolve(manifestDir);
138
133
  let parent = dir;
139
134
  do {
140
135
  dir = parent;
141
136
  if (fs.existsSync(path.join(dir, 'Cargo.lock'))) {
142
- return true;
137
+ return dir;
143
138
  }
144
139
  // If this directory has a Cargo.toml with [workspace], the lock file
145
140
  // should have been here — stop searching.
@@ -148,7 +143,7 @@ function validateLockFile(manifestDir, opts = {}) {
148
143
  try {
149
144
  let content = fs.readFileSync(cargoToml, 'utf-8');
150
145
  if (/\[workspace\]/.test(content)) {
151
- return false;
146
+ return null;
152
147
  }
153
148
  }
154
149
  catch (_) {
@@ -157,7 +152,23 @@ function validateLockFile(manifestDir, opts = {}) {
157
152
  }
158
153
  parent = path.dirname(dir);
159
154
  } while (parent !== dir);
160
- return false;
155
+ return null;
156
+ }
157
+ /**
158
+ * Validates that Cargo.lock exists in the manifest directory or in a parent
159
+ * workspace root directory. In Cargo workspaces the lock file always lives at
160
+ * the workspace root, so when a member crate's Cargo.toml is provided we walk
161
+ * up the directory tree looking for Cargo.lock (stopping when we find a
162
+ * Cargo.toml that contains a [workspace] section, or when we reach the
163
+ * filesystem root).
164
+ * When TRUSTIFY_DA_WORKSPACE_DIR is provided (via env var or opts),
165
+ * checks only that directory for Cargo.lock — no walk-up.
166
+ * @param {string} manifestDir - the directory where the manifest lies
167
+ * @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
168
+ * @returns {boolean} true if Cargo.lock is found
169
+ */
170
+ function validateLockFile(manifestDir, opts = {}) {
171
+ return _findCargoLockDir(manifestDir, opts) !== null;
161
172
  }
162
173
  /**
163
174
  * Provide content and content type for Cargo stack analysis.
@@ -201,7 +212,7 @@ function getSBOM(manifest, opts = {}, includeTransitive) {
201
212
  let ignoredDeps = getIgnoredDeps(manifest, metadata);
202
213
  let crateType = detectCrateType(metadata);
203
214
  let license = readLicenseFromManifest(manifest, metadata);
204
- let hashMap = parseCargoLockHashes(manifestDir);
215
+ let hashMap = parseCargoLockHashes(manifestDir, opts);
205
216
  let sbom;
206
217
  if (crateType === CrateType.WORKSPACE_VIRTUAL) {
207
218
  sbom = handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransitive, opts, license, hashMap);
@@ -248,15 +259,17 @@ function executeCargoMetadata(cargoBin, manifestDir) {
248
259
  /**
249
260
  * Parses Cargo.lock to extract SHA-256 checksums for each package.
250
261
  * @param {string} manifestDir - directory containing Cargo.toml (Cargo.lock is searched upward)
262
+ * @param {{TRUSTIFY_DA_WORKSPACE_DIR?: string}} [opts={}] - optional workspace root
251
263
  * @returns {Map<string, Array<{alg: string, content: string}>>} map of "name@version" to CycloneDX hashes
252
264
  * @private
253
265
  */
254
- function parseCargoLockHashes(manifestDir) {
266
+ function parseCargoLockHashes(manifestDir, opts = {}) {
255
267
  let hashMap = new Map();
256
- let lockPath = findCargoLock(manifestDir);
257
- if (!lockPath) {
268
+ let lockDir = _findCargoLockDir(manifestDir, opts);
269
+ if (!lockDir) {
258
270
  return hashMap;
259
271
  }
272
+ let lockPath = path.join(lockDir, 'Cargo.lock');
260
273
  try {
261
274
  let content = fs.readFileSync(lockPath, 'utf-8');
262
275
  let parsed = parseToml(content);
@@ -267,41 +280,10 @@ function parseCargoLockHashes(manifestDir) {
267
280
  }
268
281
  }
269
282
  catch (error) {
270
- console.warn('Failed to parse Cargo.lock for hashes, SBOM will be generated without hashes');
283
+ console.warn(`Failed to parse Cargo.lock at ${lockPath} for hashes: ${error.message}. SBOM will be generated without hashes.`);
271
284
  }
272
285
  return hashMap;
273
286
  }
274
- /**
275
- * Searches upward from the manifest directory to find the nearest Cargo.lock.
276
- * @param {string} manifestDir - starting directory
277
- * @returns {string|null} absolute path to Cargo.lock or null if not found
278
- * @private
279
- */
280
- function findCargoLock(manifestDir) {
281
- let dir = path.resolve(manifestDir);
282
- let parent = dir;
283
- do {
284
- dir = parent;
285
- let lockFile = path.join(dir, 'Cargo.lock');
286
- if (fs.existsSync(lockFile)) {
287
- return lockFile;
288
- }
289
- let cargoToml = path.join(dir, 'Cargo.toml');
290
- if (fs.existsSync(cargoToml)) {
291
- try {
292
- let content = fs.readFileSync(cargoToml, 'utf-8');
293
- if (/\[workspace\]/.test(content)) {
294
- return null;
295
- }
296
- }
297
- catch (_) {
298
- // ignore read errors, keep searching
299
- }
300
- }
301
- parent = path.dirname(dir);
302
- } while (parent !== dir);
303
- return null;
304
- }
305
287
  /**
306
288
  * Detects the type of Cargo project from metadata.
307
289
  * @param {object} metadata - parsed cargo metadata
@@ -459,7 +441,7 @@ function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, hash
459
441
  let depPurl = depPackage.source == null
460
442
  ? toPathDepPurl(depPackage.name, depPackage.version)
461
443
  : toPurl(depPackage.name, depPackage.version);
462
- let hashes = hashMap ? hashMap.get(`${depPackage.name}@${depPackage.version}`) : undefined;
444
+ let hashes = hashMap.get(`${depPackage.name}@${depPackage.version}`);
463
445
  sbom.addDependency(sourcePurl, depPurl, undefined, hashes);
464
446
  addTransitiveDeps(sbom, metadata, depId, ignoredDeps, visited, hashMap);
465
447
  }
@@ -491,7 +473,7 @@ function addDirectDeps(sbom, metadata, packageId, parentPurl, ignoredDeps, hashM
491
473
  let depPurl = depPackage.source == null
492
474
  ? toPathDepPurl(depPackage.name, depPackage.version)
493
475
  : toPurl(depPackage.name, depPackage.version);
494
- let hashes = hashMap ? hashMap.get(`${depPackage.name}@${depPackage.version}`) : undefined;
476
+ let hashes = hashMap.get(`${depPackage.name}@${depPackage.version}`);
495
477
  sbom.addDependency(parentPurl, depPurl, undefined, hashes);
496
478
  }
497
479
  }