@trustify-da/trustify-da-javascript-client 0.3.0-ea.7ed8d8c → 0.3.0-ea.7ee4047

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 (98) hide show
  1. package/README.md +388 -20
  2. package/dist/package.json +39 -26
  3. package/dist/src/analysis.d.ts +58 -20
  4. package/dist/src/analysis.js +134 -78
  5. package/dist/src/batch_opts.d.ts +24 -0
  6. package/dist/src/batch_opts.js +35 -0
  7. package/dist/src/cli.js +427 -12
  8. package/dist/src/config.d.ts +120 -0
  9. package/dist/src/config.js +260 -0
  10. package/dist/src/cyclone_dx_sbom.d.ts +30 -2
  11. package/dist/src/cyclone_dx_sbom.js +95 -9
  12. package/dist/src/index.d.ts +154 -16
  13. package/dist/src/index.js +372 -36
  14. package/dist/src/license/index.d.ts +28 -0
  15. package/dist/src/license/index.js +106 -0
  16. package/dist/src/license/license_utils.d.ts +40 -0
  17. package/dist/src/license/license_utils.js +137 -0
  18. package/dist/src/license/licenses_api.d.ts +34 -0
  19. package/dist/src/license/licenses_api.js +98 -0
  20. package/dist/src/license/project_license.d.ts +20 -0
  21. package/dist/src/license/project_license.js +62 -0
  22. package/dist/src/oci_image/images.d.ts +1 -1
  23. package/dist/src/oci_image/images.js +15 -1
  24. package/dist/src/oci_image/utils.d.ts +5 -5
  25. package/dist/src/oci_image/utils.js +11 -2
  26. package/dist/src/package_version.d.ts +8 -0
  27. package/dist/src/package_version.js +31 -0
  28. package/dist/src/provider.d.ts +16 -3
  29. package/dist/src/provider.js +33 -5
  30. package/dist/src/providers/base_java.d.ts +67 -12
  31. package/dist/src/providers/base_java.js +82 -61
  32. package/dist/src/providers/base_javascript.d.ts +99 -12
  33. package/dist/src/providers/base_javascript.js +244 -28
  34. package/dist/src/providers/base_pyproject.d.ts +158 -0
  35. package/dist/src/providers/base_pyproject.js +322 -0
  36. package/dist/src/providers/containerfile_parser.d.ts +5 -0
  37. package/dist/src/providers/containerfile_parser.js +20 -0
  38. package/dist/src/providers/golang_gomodules.d.ts +30 -13
  39. package/dist/src/providers/golang_gomodules.js +219 -122
  40. package/dist/src/providers/gomod_parser.d.ts +4 -0
  41. package/dist/src/providers/gomod_parser.js +16 -0
  42. package/dist/src/providers/java_gradle.d.ts +73 -0
  43. package/dist/src/providers/java_gradle.js +331 -23
  44. package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
  45. package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
  46. package/dist/src/providers/java_maven.d.ts +44 -9
  47. package/dist/src/providers/java_maven.js +299 -13
  48. package/dist/src/providers/javascript_bun.d.ts +22 -0
  49. package/dist/src/providers/javascript_bun.js +141 -0
  50. package/dist/src/providers/javascript_npm.d.ts +19 -0
  51. package/dist/src/providers/javascript_npm.js +61 -1
  52. package/dist/src/providers/javascript_pnpm.d.ts +14 -1
  53. package/dist/src/providers/javascript_pnpm.js +55 -5
  54. package/dist/src/providers/javascript_yarn.d.ts +12 -0
  55. package/dist/src/providers/javascript_yarn.js +75 -2
  56. package/dist/src/providers/manifest.d.ts +2 -0
  57. package/dist/src/providers/manifest.js +28 -7
  58. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  59. package/dist/src/providers/marker_evaluator.js +191 -0
  60. package/dist/src/providers/oci_dockerfile.d.ts +51 -0
  61. package/dist/src/providers/oci_dockerfile.js +177 -0
  62. package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
  63. package/dist/src/providers/processors/yarn_berry_processor.js +91 -7
  64. package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
  65. package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
  66. package/dist/src/providers/python_controller.d.ts +5 -1
  67. package/dist/src/providers/python_controller.js +69 -8
  68. package/dist/src/providers/python_pip.d.ts +12 -0
  69. package/dist/src/providers/python_pip.js +18 -8
  70. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  71. package/dist/src/providers/python_pip_pyproject.js +146 -0
  72. package/dist/src/providers/python_poetry.d.ts +98 -0
  73. package/dist/src/providers/python_poetry.js +286 -0
  74. package/dist/src/providers/python_uv.d.ts +46 -0
  75. package/dist/src/providers/python_uv.js +275 -0
  76. package/dist/src/providers/requirements_parser.js +6 -9
  77. package/dist/src/providers/rust_cargo.d.ts +56 -0
  78. package/dist/src/providers/rust_cargo.js +682 -0
  79. package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
  80. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  81. package/dist/src/providers/tree-sitter-requirements.wasm +0 -0
  82. package/dist/src/remediate.d.ts +107 -0
  83. package/dist/src/remediate.js +244 -0
  84. package/dist/src/remediation.d.ts +81 -0
  85. package/dist/src/remediation.js +475 -0
  86. package/dist/src/remediation_report.d.ts +23 -0
  87. package/dist/src/remediation_report.js +180 -0
  88. package/dist/src/sbom.d.ts +28 -2
  89. package/dist/src/sbom.js +26 -4
  90. package/dist/src/tools.d.ts +53 -11
  91. package/dist/src/tools.js +147 -13
  92. package/dist/src/updaters/maven_updater.d.ts +58 -0
  93. package/dist/src/updaters/maven_updater.js +345 -0
  94. package/dist/src/updaters/toml_updater.d.ts +62 -0
  95. package/dist/src/updaters/toml_updater.js +276 -0
  96. package/dist/src/workspace.d.ts +71 -0
  97. package/dist/src/workspace.js +257 -0
  98. package/package.json +40 -27
@@ -0,0 +1,260 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { load as yamlLoad } from 'js-yaml';
4
+ /**
5
+ * @typedef {{
6
+ * 'group-by'?: 'dependency' | 'bundle',
7
+ * exclude?: string[],
8
+ * labels?: string[],
9
+ * 'branch-prefix'?: string,
10
+ * [key: string]: unknown
11
+ * }} RemediationConfig
12
+ */
13
+ /** @typedef {{ critical?: number, high?: number, 'license-conflicts'?: number, [key: string]: unknown }} FailOnConfig */
14
+ /** @typedef {{ 'fail-on'?: FailOnConfig, [key: string]: unknown }} CheckConfig */
15
+ /** @typedef {{ format?: string, targets?: string[], [key: string]: unknown }} SbomConfig */
16
+ /**
17
+ * @typedef {{
18
+ * 'backend-url'?: string,
19
+ * providers?: string[] | string,
20
+ * sources?: string[] | string,
21
+ * remediation?: RemediationConfig,
22
+ * check?: CheckConfig,
23
+ * sbom?: SbomConfig,
24
+ * [key: string]: unknown
25
+ * }} FileConfig
26
+ */
27
+ /**
28
+ * @typedef {{
29
+ * backendUrl: string | null,
30
+ * backendUrlSource: 'cli' | 'environment' | 'file' | 'default',
31
+ * providers: string[],
32
+ * sources: string[],
33
+ * groupBy: string,
34
+ * remediation: object,
35
+ * check: object,
36
+ * sbom: object
37
+ * }} ResolvedConfig
38
+ */
39
+ /** Config file names discovered by walking up the directory tree, in precedence order. */
40
+ export const CONFIG_FILENAMES = ['.trustify-da.yml', '.trustify-da.yaml'];
41
+ function isMapping(value) {
42
+ if (value == null || typeof value !== 'object' || Array.isArray(value)) {
43
+ return false;
44
+ }
45
+ const prototype = Object.getPrototypeOf(value);
46
+ return prototype === Object.prototype || prototype == null;
47
+ }
48
+ function assertMapping(value, field, source) {
49
+ if (!isMapping(value)) {
50
+ throw new Error(`Invalid config file ${source}: ${field} must be a mapping`);
51
+ }
52
+ }
53
+ function assertString(value, field, source) {
54
+ if (typeof value !== 'string') {
55
+ throw new Error(`Invalid config file ${source}: ${field} must be a string`);
56
+ }
57
+ }
58
+ function assertStringArray(value, field, source) {
59
+ if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
60
+ throw new Error(`Invalid config file ${source}: ${field} must be an array of strings`);
61
+ }
62
+ }
63
+ function assertStringOrArray(value, field, source) {
64
+ if (typeof value !== 'string') {
65
+ assertStringArray(value, field, source);
66
+ }
67
+ }
68
+ /**
69
+ * Validates all fields in the initial configuration schema. Unknown fields are preserved.
70
+ * @param {unknown} config
71
+ * @param {string} [source]
72
+ * @returns {FileConfig}
73
+ */
74
+ function validateConfig(config, source = '<config>') {
75
+ assertMapping(config, 'root', source);
76
+ if (config['backend-url'] !== undefined) {
77
+ assertString(config['backend-url'], 'backend-url', source);
78
+ }
79
+ if (config.providers !== undefined) {
80
+ assertStringOrArray(config.providers, 'providers', source);
81
+ }
82
+ if (config.sources !== undefined) {
83
+ assertStringOrArray(config.sources, 'sources', source);
84
+ }
85
+ if (config.remediation !== undefined) {
86
+ assertMapping(config.remediation, 'remediation', source);
87
+ const remediation = config.remediation;
88
+ if (remediation['group-by'] !== undefined && !['dependency', 'bundle'].includes(remediation['group-by'])) {
89
+ throw new Error(`Invalid config file ${source}: remediation.group-by must be dependency or bundle`);
90
+ }
91
+ if (remediation.exclude !== undefined) {
92
+ assertStringArray(remediation.exclude, 'remediation.exclude', source);
93
+ }
94
+ if (remediation.labels !== undefined) {
95
+ assertStringArray(remediation.labels, 'remediation.labels', source);
96
+ }
97
+ if (remediation['branch-prefix'] !== undefined) {
98
+ assertString(remediation['branch-prefix'], 'remediation.branch-prefix', source);
99
+ }
100
+ }
101
+ if (config.check !== undefined) {
102
+ assertMapping(config.check, 'check', source);
103
+ if (config.check['fail-on'] !== undefined) {
104
+ assertMapping(config.check['fail-on'], 'check.fail-on', source);
105
+ for (const field of ['critical', 'high', 'license-conflicts']) {
106
+ const value = config.check['fail-on'][field];
107
+ if (value !== undefined && (typeof value !== 'number' || !Number.isFinite(value))) {
108
+ throw new Error(`Invalid config file ${source}: check.fail-on.${field} must be a finite number`);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ if (config.sbom !== undefined) {
114
+ assertMapping(config.sbom, 'sbom', source);
115
+ if (config.sbom.format !== undefined) {
116
+ assertString(config.sbom.format, 'sbom.format', source);
117
+ }
118
+ if (config.sbom.targets !== undefined) {
119
+ assertStringArray(config.sbom.targets, 'sbom.targets', source);
120
+ }
121
+ }
122
+ return config;
123
+ }
124
+ /**
125
+ * Walks up from `startPath` looking for a `.trustify-da.yml` (or `.yaml`) file,
126
+ * similar to how `.eslintrc` discovery works. If `startPath` points at a file,
127
+ * discovery begins in its containing directory. Within a directory, `.yml` takes
128
+ * precedence over `.yaml`.
129
+ * @param {string} startPath - manifest file or directory to start the search from
130
+ * @returns {string | null} absolute path to the config file, or null if none found
131
+ */
132
+ function findConfigFile(startPath) {
133
+ let dir = path.resolve(startPath || '.');
134
+ try {
135
+ if (fs.statSync(dir).isFile()) {
136
+ dir = path.dirname(dir);
137
+ }
138
+ }
139
+ catch {
140
+ // startPath may not exist yet — walk up from its resolved location anyway
141
+ }
142
+ // Walk up until the filesystem root (where dirname(dir) === dir)
143
+ for (;;) {
144
+ for (const name of CONFIG_FILENAMES) {
145
+ const candidate = path.join(dir, name);
146
+ try {
147
+ if (fs.statSync(candidate).isFile()) {
148
+ return candidate;
149
+ }
150
+ }
151
+ catch {
152
+ // Ignore inaccessible or disappearing candidates and continue discovery.
153
+ }
154
+ }
155
+ const parent = path.dirname(dir);
156
+ if (parent === dir) {
157
+ return null;
158
+ }
159
+ dir = parent;
160
+ }
161
+ }
162
+ /**
163
+ * Discovers and parses `.trustify-da.yml` by walking up from `startPath`.
164
+ * A missing config file is not an error — an empty object is returned so callers
165
+ * can fall back to defaults.
166
+ * @param {string} [startPath] - manifest file or directory to start the search from
167
+ * @returns {FileConfig} the parsed config object, or `{}` when no file is found
168
+ * @throws {Error} when the config file exists but contains malformed YAML
169
+ */
170
+ export function loadConfig(startPath) {
171
+ const configPath = findConfigFile(startPath);
172
+ if (!configPath) {
173
+ return {};
174
+ }
175
+ const content = fs.readFileSync(configPath, 'utf-8');
176
+ let doc;
177
+ try {
178
+ doc = yamlLoad(content);
179
+ }
180
+ catch (err) {
181
+ throw new Error(`Failed to parse config file ${configPath}: ${err.message}`);
182
+ }
183
+ return doc == null ? {} : validateConfig(doc, configPath);
184
+ }
185
+ /**
186
+ * Normalizes a providers/sources value to an array of trimmed strings. Accepts
187
+ * either a YAML array (`[redhat, osv]`) or a comma-separated string (`redhat,osv`).
188
+ * @param {string[] | string | undefined | null} value
189
+ * @returns {string[]}
190
+ */
191
+ function toArray(value) {
192
+ if (Array.isArray(value)) {
193
+ return value.filter(v => typeof v === 'string' && v.trim()).map(v => v.trim());
194
+ }
195
+ if (typeof value === 'string') {
196
+ return value.split(',').map(v => v.trim()).filter(Boolean);
197
+ }
198
+ return [];
199
+ }
200
+ /**
201
+ * Returns the first argument that is neither undefined nor null.
202
+ * @param {...unknown} values
203
+ * @returns {unknown}
204
+ */
205
+ function first(...values) {
206
+ return values.find(v => v != null);
207
+ }
208
+ /**
209
+ * Merges config sources with precedence: CLI flag > environment variable
210
+ * (`TRUSTIFY_DA_*`) > config file > hardcoded default. Providers and sources are
211
+ * normalized to arrays regardless of whether the source used a YAML array or a
212
+ * comma-separated string.
213
+ * @param {FileConfig} [fileConfig={}] - values parsed from `.trustify-da.yml`
214
+ * @param {{ providers?: string, sources?: string, groupBy?: string, backendUrl?: string }} [cliFlags={}]
215
+ * @param {{ [key: string]: string | undefined }} [envVars={}] - typically `process.env`
216
+ * @returns {ResolvedConfig} the resolved, typed config object
217
+ */
218
+ export function mergeConfig(fileConfig = {}, cliFlags = {}, envVars = {}) {
219
+ const file = validateConfig(fileConfig || {});
220
+ const cli = cliFlags || {};
221
+ const env = envVars || {};
222
+ let backendUrl = null;
223
+ let backendUrlSource = 'default';
224
+ if (cli.backendUrl != null) {
225
+ backendUrl = cli.backendUrl;
226
+ backendUrlSource = 'cli';
227
+ }
228
+ else if (env.TRUSTIFY_DA_BACKEND_URL != null) {
229
+ backendUrl = env.TRUSTIFY_DA_BACKEND_URL;
230
+ backendUrlSource = 'environment';
231
+ }
232
+ else if (file['backend-url'] != null) {
233
+ backendUrl = file['backend-url'];
234
+ backendUrlSource = 'file';
235
+ }
236
+ const groupBy = first(cli.groupBy, env.TRUSTIFY_DA_GROUP_BY, file.remediation?.['group-by']) ?? 'dependency';
237
+ if (!['dependency', 'bundle'].includes(groupBy)) {
238
+ throw new Error(`Invalid group-by value "${groupBy}". Expected dependency or bundle.`);
239
+ }
240
+ return {
241
+ backendUrl,
242
+ backendUrlSource,
243
+ providers: toArray(first(cli.providers, env.TRUSTIFY_DA_PROVIDERS, file.providers)),
244
+ sources: toArray(first(cli.sources, env.TRUSTIFY_DA_SOURCES, file.sources)),
245
+ groupBy,
246
+ remediation: file.remediation ?? {},
247
+ check: file.check ?? {},
248
+ sbom: file.sbom ?? {},
249
+ };
250
+ }
251
+ /**
252
+ * Loads and merges the project configuration for a target path.
253
+ * @param {string} [startPath] - manifest file or directory to start discovery from
254
+ * @param {{ providers?: string, sources?: string, groupBy?: string, backendUrl?: string }} [cliFlags={}] - explicit options
255
+ * @param {{ [key: string]: string | undefined }} [envVars={}] - typically `process.env`
256
+ * @returns {ResolvedConfig} the resolved, typed config object
257
+ */
258
+ export function resolveConfig(startPath, cliFlags = {}, envVars = {}) {
259
+ return mergeConfig(loadConfig(startPath), cliFlags, envVars);
260
+ }
@@ -6,9 +6,10 @@ export default class CycloneDxSbom {
6
6
  sourceManifestForAuditTrail: any;
7
7
  /**
8
8
  * @param {PackageURL} root - add main/root component for sbom
9
+ * @param {string|Array} [licenses] - optional license(s) for the root component
9
10
  * @return {CycloneDxSbom} the CycloneDxSbom Sbom Object
10
11
  */
11
- addRoot(root: PackageURL): CycloneDxSbom;
12
+ addRoot(root: PackageURL, licenses?: string | any[]): CycloneDxSbom;
12
13
  /**
13
14
  * @return {{{"bom-ref": string, name, purl: string, type, version}}} root component of sbom.
14
15
  */
@@ -17,9 +18,27 @@ export default class CycloneDxSbom {
17
18
  * Adds a dependency relationship between two components in the SBOM
18
19
  * @param {PackageURL} sourceRef - The source component (parent)
19
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
20
23
  * @return {CycloneDxSbom} The updated SBOM
21
24
  */
22
- 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;
29
+ /**
30
+ * Attach hashes to already-added components by matching their PURL. This is a
31
+ * post-processing step so ecosystem-specific hash sources (e.g. Maven reading
32
+ * the local .m2 cache) can enrich the SBOM without threading their concern
33
+ * through the shared dependency-tree parser. Components without a matching
34
+ * entry, or whose hashes are already set, are left untouched.
35
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - PURL→hashes map
36
+ * @return {CycloneDxSbom} the updated SBOM
37
+ */
38
+ attachHashes(hashMap: Map<string, Array<{
39
+ alg: string;
40
+ content: string;
41
+ }>>): CycloneDxSbom;
23
42
  /** @param {{}} opts - various options, settings and configuration of application.
24
43
  * @return String CycloneDx Sbom json object in a string format
25
44
  */
@@ -48,6 +67,8 @@ export default class CycloneDxSbom {
48
67
  type: any;
49
68
  version: any;
50
69
  scope: any;
70
+ licenses?: any;
71
+ hashes?: any;
51
72
  };
52
73
  /**
53
74
  * This method gets an array of dependencies to be ignored, and remove all of them from CycloneDx Sbom
@@ -68,6 +89,13 @@ export default class CycloneDxSbom {
68
89
  * @return {boolean}
69
90
  */
70
91
  checkIfPackageInsideDependsOnList(component: any, name: string): boolean;
92
+ /**
93
+ * Checks if any entry in the dependsOn list of sourceRef starts with the given purl prefix.
94
+ * @param {PackageURL} sourceRef - The source component
95
+ * @param {string} purlPrefix - The purl prefix to match (e.g. "pkg:npm/minimist@")
96
+ * @return {boolean}
97
+ */
98
+ checkDependsOnByPurlPrefix(sourceRef: PackageURL, purlPrefix: string): boolean;
71
99
  /** Removes the root component from the sbom
72
100
  */
73
101
  removeRootComponent(): void;
@@ -1,14 +1,23 @@
1
1
  import { EOL } from "os";
2
2
  import { PackageURL } from "packageurl-js";
3
+ import { getPackageVersion } from "./package_version.js";
4
+ /**
5
+ * This client's version, read once at module load, used to populate
6
+ * metadata.tools.components[].version in the generated SBOM.
7
+ * @type {string|undefined}
8
+ */
9
+ const PACKAGE_VERSION = getPackageVersion();
3
10
  /**
4
11
  *
5
12
  * @param component {PackageURL}
6
13
  * @param type type of package - application or library
7
14
  * @param scope scope of the component - runtime or compile
8
- * @return {{"bom-ref": string, name, purl: string, type, version, scope}}
15
+ * @param licenses optional license string or array of licenses for the component
16
+ * @param hashes optional array of hash objects for the component, e.g. [{alg: "SHA-256", content: "..."}]
17
+ * @return {{"bom-ref": string, name, purl: string, type, version, scope, licenses?, hashes?}}
9
18
  * @private
10
19
  */
11
- function getComponent(component, type, scope) {
20
+ function getComponent(component, type, scope, licenses, hashes) {
12
21
  let componentObject;
13
22
  if (component instanceof PackageURL) {
14
23
  if (component.namespace) {
@@ -36,6 +45,28 @@ function getComponent(component, type, scope) {
36
45
  else {
37
46
  componentObject = component;
38
47
  }
48
+ // Add licenses if provided (CycloneDX format). Callers must provide valid SPDX identifiers.
49
+ if (licenses) {
50
+ const licenseArray = Array.isArray(licenses) ? licenses : [licenses];
51
+ const validLicenses = licenseArray
52
+ .map(lic => {
53
+ if (typeof lic === 'string') {
54
+ return { license: { id: lic } };
55
+ }
56
+ if (typeof lic === 'object' && lic !== null && ('license' in lic || 'expression' in lic)) {
57
+ return lic;
58
+ }
59
+ return null;
60
+ })
61
+ .filter(Boolean);
62
+ if (validLicenses.length > 0) {
63
+ componentObject.licenses = validLicenses;
64
+ }
65
+ }
66
+ // Add hashes if provided (CycloneDX 1.4 format).
67
+ if (hashes && hashes.length > 0) {
68
+ componentObject.hashes = hashes;
69
+ }
39
70
  return componentObject;
40
71
  }
41
72
  function createDependency(dependency) {
@@ -56,11 +87,12 @@ export default class CycloneDxSbom {
56
87
  }
57
88
  /**
58
89
  * @param {PackageURL} root - add main/root component for sbom
90
+ * @param {string|Array} [licenses] - optional license(s) for the root component
59
91
  * @return {CycloneDxSbom} the CycloneDxSbom Sbom Object
60
92
  */
61
- addRoot(root) {
93
+ addRoot(root, licenses) {
62
94
  this.rootComponent =
63
- getComponent(root, "application");
95
+ getComponent(root, "application", undefined, licenses);
64
96
  this.components.push(this.rootComponent);
65
97
  return this;
66
98
  }
@@ -74,16 +106,24 @@ export default class CycloneDxSbom {
74
106
  * Adds a dependency relationship between two components in the SBOM
75
107
  * @param {PackageURL} sourceRef - The source component (parent)
76
108
  * @param {PackageURL} targetRef - The target component (dependency)
109
+ * @param {string} [scope] - Scope of the dependency
110
+ * @param {Array<{alg: string, content: string}>} [targetHashes] - Optional hashes for the target component
77
111
  * @return {CycloneDxSbom} The updated SBOM
78
112
  */
79
- addDependency(sourceRef, targetRef, scope) {
113
+ addDependency(sourceRef, targetRef, scope, targetHashes) {
80
114
  const sourcePurl = sourceRef.toString();
81
115
  const targetPurl = targetRef.toString();
82
116
  // Ensure both components exist in the components list
83
117
  [sourceRef, targetRef].forEach((ref, index) => {
84
118
  const purl = index === 0 ? sourcePurl : targetPurl;
85
- if (this.getComponentIndex(purl) < 0) {
86
- this.components.push(getComponent(ref, "library", scope));
119
+ const existingIndex = this.getComponentIndex(purl);
120
+ if (existingIndex < 0) {
121
+ const hashes = index === 1 ? targetHashes : undefined;
122
+ this.components.push(getComponent(ref, "library", scope, undefined, hashes));
123
+ }
124
+ else if (index === 1 && targetHashes && targetHashes.length > 0 && !this.components[existingIndex].hashes) {
125
+ // Update hashes if the component was first seen without them (e.g. as a source)
126
+ this.components[existingIndex].hashes = targetHashes;
87
127
  }
88
128
  });
89
129
  // Ensure source dependency exists
@@ -102,22 +142,54 @@ export default class CycloneDxSbom {
102
142
  }
103
143
  return this;
104
144
  }
145
+ /**
146
+ * Attach hashes to already-added components by matching their PURL. This is a
147
+ * post-processing step so ecosystem-specific hash sources (e.g. Maven reading
148
+ * the local .m2 cache) can enrich the SBOM without threading their concern
149
+ * through the shared dependency-tree parser. Components without a matching
150
+ * entry, or whose hashes are already set, are left untouched.
151
+ * @param {Map<string, Array<{alg: string, content: string}>>} hashMap - PURL→hashes map
152
+ * @return {CycloneDxSbom} the updated SBOM
153
+ */
154
+ attachHashes(hashMap) {
155
+ if (!hashMap || hashMap.size === 0) {
156
+ return this;
157
+ }
158
+ for (const component of this.components) {
159
+ if (component.hashes) {
160
+ continue;
161
+ }
162
+ const hashes = hashMap.get(component.purl);
163
+ if (hashes && hashes.length > 0) {
164
+ component.hashes = hashes;
165
+ }
166
+ }
167
+ return this;
168
+ }
105
169
  /** @param {{}} opts - various options, settings and configuration of application.
106
170
  * @return String CycloneDx Sbom json object in a string format
107
171
  */
108
172
  getAsJsonString(opts) {
109
173
  let manifestType = opts["manifest-type"];
110
174
  this.setSourceManifest(opts["source-manifest"]);
175
+ const rootPurl = this.rootComponent?.purl;
111
176
  this.sbomObject = {
112
177
  "bomFormat": "CycloneDX",
113
- "specVersion": "1.4",
178
+ "specVersion": "1.6",
114
179
  "version": 1,
115
180
  "metadata": {
116
181
  "timestamp": new Date(),
117
182
  "component": this.rootComponent,
183
+ "tools": {
184
+ "components": [{
185
+ "type": "application",
186
+ "name": "trustify-da-javascript-client",
187
+ "version": PACKAGE_VERSION
188
+ }]
189
+ },
118
190
  "properties": new Array()
119
191
  },
120
- "components": this.components,
192
+ "components": this.components.filter(c => c.purl !== rootPurl),
121
193
  "dependencies": this.dependencies
122
194
  };
123
195
  if (this.rootComponent === undefined) {
@@ -229,6 +301,20 @@ export default class CycloneDxSbom {
229
301
  return false;
230
302
  }
231
303
  }
304
+ /**
305
+ * Checks if any entry in the dependsOn list of sourceRef starts with the given purl prefix.
306
+ * @param {PackageURL} sourceRef - The source component
307
+ * @param {string} purlPrefix - The purl prefix to match (e.g. "pkg:npm/minimist@")
308
+ * @return {boolean}
309
+ */
310
+ checkDependsOnByPurlPrefix(sourceRef, purlPrefix) {
311
+ const sourcePurl = sourceRef.toString();
312
+ const depIndex = this.getDependencyIndex(sourcePurl);
313
+ if (depIndex < 0) {
314
+ return false;
315
+ }
316
+ return this.dependencies[depIndex].dependsOn.some(dep => dep.startsWith(purlPrefix));
317
+ }
232
318
  /** Removes the root component from the sbom
233
319
  */
234
320
  removeRootComponent() {