@cyclonedx/cdxgen 12.3.2 → 12.4.0

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 (182) hide show
  1. package/README.md +70 -22
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +238 -116
  4. package/bin/convert.js +28 -13
  5. package/bin/hbom.js +490 -0
  6. package/bin/repl.js +580 -29
  7. package/bin/validate.js +34 -4
  8. package/bin/verify.js +40 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/predictive-audit-allowlist.json +11 -0
  13. package/data/queries-darwin.json +12 -1
  14. package/data/queries-win.json +7 -1
  15. package/data/queries.json +39 -2
  16. package/data/rules/ai-agent-governance.yaml +16 -0
  17. package/data/rules/asar-archives.yaml +150 -0
  18. package/data/rules/chrome-extensions.yaml +8 -0
  19. package/data/rules/ci-permissions.yaml +171 -15
  20. package/data/rules/container-risk.yaml +14 -7
  21. package/data/rules/dependency-sources.yaml +76 -5
  22. package/data/rules/hbom-compliance.yaml +325 -0
  23. package/data/rules/hbom-performance.yaml +307 -0
  24. package/data/rules/hbom-security.yaml +248 -0
  25. package/data/rules/host-topology.yaml +165 -0
  26. package/data/rules/mcp-servers.yaml +18 -3
  27. package/data/rules/obom-runtime.yaml +907 -22
  28. package/data/rules/package-integrity.yaml +36 -0
  29. package/data/rules/rootfs-hardening.yaml +179 -0
  30. package/data/rules/vscode-extensions.yaml +9 -0
  31. package/lib/audit/index.js +209 -8
  32. package/lib/audit/index.poku.js +332 -0
  33. package/lib/audit/reporters.js +222 -0
  34. package/lib/audit/targets.js +146 -1
  35. package/lib/audit/targets.poku.js +186 -0
  36. package/lib/cli/asar.poku.js +328 -0
  37. package/lib/cli/index.js +647 -127
  38. package/lib/cli/index.poku.js +1905 -187
  39. package/lib/evinser/evinser.js +14 -9
  40. package/lib/helpers/agentFormulationParser.js +6 -2
  41. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  42. package/lib/helpers/analyzer.js +1444 -38
  43. package/lib/helpers/analyzer.poku.js +409 -0
  44. package/lib/helpers/analyzerScope.js +712 -0
  45. package/lib/helpers/asarutils.js +1556 -0
  46. package/lib/helpers/asarutils.poku.js +443 -0
  47. package/lib/helpers/auditCategories.js +12 -0
  48. package/lib/helpers/auditCategories.poku.js +32 -0
  49. package/lib/helpers/cbomutils.js +271 -1
  50. package/lib/helpers/cbomutils.poku.js +248 -5
  51. package/lib/helpers/chromextutils.js +25 -3
  52. package/lib/helpers/chromextutils.poku.js +68 -0
  53. package/lib/helpers/ciParsers/githubActions.js +79 -0
  54. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  55. package/lib/helpers/communityAiConfigParser.js +15 -5
  56. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  57. package/lib/helpers/depsUtils.js +5 -0
  58. package/lib/helpers/depsUtils.poku.js +55 -0
  59. package/lib/helpers/display.js +336 -23
  60. package/lib/helpers/display.poku.js +179 -43
  61. package/lib/helpers/evidenceUtils.js +58 -0
  62. package/lib/helpers/evidenceUtils.poku.js +54 -0
  63. package/lib/helpers/exportUtils.js +9 -0
  64. package/lib/helpers/gtfobins.js +142 -8
  65. package/lib/helpers/gtfobins.poku.js +24 -1
  66. package/lib/helpers/hbom.js +710 -0
  67. package/lib/helpers/hbom.poku.js +496 -0
  68. package/lib/helpers/hbomAnalysis.js +268 -0
  69. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  70. package/lib/helpers/hbomLoader.js +35 -0
  71. package/lib/helpers/hostTopology.js +803 -0
  72. package/lib/helpers/hostTopology.poku.js +363 -0
  73. package/lib/helpers/inventoryStats.js +69 -0
  74. package/lib/helpers/inventoryStats.poku.js +86 -0
  75. package/lib/helpers/lolbas.js +19 -1
  76. package/lib/helpers/lolbas.poku.js +23 -0
  77. package/lib/helpers/mcpConfigParser.js +21 -5
  78. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  79. package/lib/helpers/osqueryTransform.js +47 -0
  80. package/lib/helpers/osqueryTransform.poku.js +47 -0
  81. package/lib/helpers/plugins.js +349 -0
  82. package/lib/helpers/plugins.poku.js +57 -0
  83. package/lib/helpers/propertySanitizer.js +121 -0
  84. package/lib/helpers/protobom.js +156 -45
  85. package/lib/helpers/protobom.poku.js +140 -5
  86. package/lib/helpers/remote/dependency-track.js +36 -3
  87. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  88. package/lib/helpers/source.js +24 -0
  89. package/lib/helpers/source.poku.js +32 -0
  90. package/lib/helpers/utils.js +2454 -198
  91. package/lib/helpers/utils.poku.js +1798 -74
  92. package/lib/managers/binary.e2e.poku.js +367 -0
  93. package/lib/managers/binary.js +2306 -350
  94. package/lib/managers/binary.poku.js +1700 -1
  95. package/lib/managers/docker.js +441 -95
  96. package/lib/managers/docker.poku.js +1479 -14
  97. package/lib/server/server.js +2 -24
  98. package/lib/server/server.poku.js +36 -1
  99. package/lib/stages/postgen/annotator.js +38 -0
  100. package/lib/stages/postgen/annotator.poku.js +107 -1
  101. package/lib/stages/postgen/auditBom.js +121 -18
  102. package/lib/stages/postgen/auditBom.poku.js +2967 -990
  103. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  104. package/lib/stages/postgen/postgen.js +192 -1
  105. package/lib/stages/postgen/postgen.poku.js +321 -0
  106. package/lib/stages/postgen/ruleEngine.js +116 -0
  107. package/lib/stages/pregen/envAudit.js +14 -3
  108. package/package.json +24 -21
  109. package/types/bin/hbom.d.ts +3 -0
  110. package/types/bin/hbom.d.ts.map +1 -0
  111. package/types/bin/repl.d.ts.map +1 -1
  112. package/types/lib/audit/index.d.ts +44 -0
  113. package/types/lib/audit/index.d.ts.map +1 -1
  114. package/types/lib/audit/reporters.d.ts +16 -0
  115. package/types/lib/audit/reporters.d.ts.map +1 -1
  116. package/types/lib/audit/targets.d.ts.map +1 -1
  117. package/types/lib/cli/index.d.ts +16 -0
  118. package/types/lib/cli/index.d.ts.map +1 -1
  119. package/types/lib/evinser/evinser.d.ts +4 -0
  120. package/types/lib/evinser/evinser.d.ts.map +1 -1
  121. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  122. package/types/lib/helpers/analyzer.d.ts +33 -0
  123. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  124. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  125. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  126. package/types/lib/helpers/asarutils.d.ts +34 -0
  127. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  128. package/types/lib/helpers/auditCategories.d.ts +5 -0
  129. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  130. package/types/lib/helpers/cbomutils.d.ts +3 -2
  131. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  132. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  133. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  134. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  135. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  136. package/types/lib/helpers/display.d.ts +1 -0
  137. package/types/lib/helpers/display.d.ts.map +1 -1
  138. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  139. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  141. package/types/lib/helpers/gtfobins.d.ts +8 -0
  142. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  143. package/types/lib/helpers/hbom.d.ts +49 -0
  144. package/types/lib/helpers/hbom.d.ts.map +1 -0
  145. package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
  146. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  147. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  148. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  149. package/types/lib/helpers/hostTopology.d.ts +12 -0
  150. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  151. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  152. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  153. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  154. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  155. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  156. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  157. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  158. package/types/lib/helpers/plugins.d.ts +58 -0
  159. package/types/lib/helpers/plugins.d.ts.map +1 -0
  160. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  161. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  162. package/types/lib/helpers/protobom.d.ts +3 -4
  163. package/types/lib/helpers/protobom.d.ts.map +1 -1
  164. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  165. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  166. package/types/lib/helpers/source.d.ts.map +1 -1
  167. package/types/lib/helpers/utils.d.ts +74 -8
  168. package/types/lib/helpers/utils.d.ts.map +1 -1
  169. package/types/lib/managers/binary.d.ts +5 -0
  170. package/types/lib/managers/binary.d.ts.map +1 -1
  171. package/types/lib/managers/docker.d.ts +3 -0
  172. package/types/lib/managers/docker.d.ts.map +1 -1
  173. package/types/lib/server/server.d.ts +2 -0
  174. package/types/lib/server/server.d.ts.map +1 -1
  175. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  176. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  177. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  178. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  179. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  180. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  182. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -0,0 +1,121 @@
1
+ import path from "node:path";
2
+
3
+ const DANGEROUS_OBJECT_KEYS = new Set([
4
+ "__proto__",
5
+ "constructor",
6
+ "prototype",
7
+ ]);
8
+ const INLINE_CREDENTIAL_PATTERNS = [
9
+ /\bAKIA[0-9A-Z]{16}\b/gu,
10
+ /\bbearer\s+[a-z0-9._-]{16,}\b/giu,
11
+ /\b(?:sk|rk|pk)_[a-z0-9_-]{8,}\b/giu,
12
+ /\bgh[pousr]_[a-z0-9]{20,}\b/giu,
13
+ /\bAIza[0-9A-Za-z_-]{20,}\b/gu,
14
+ ];
15
+ const JSON_PROPERTY_NAMES = new Set([
16
+ "cdx:agent:permission",
17
+ "cdx:mcp:toolAnnotations",
18
+ "cdx:skill:metadata",
19
+ ]);
20
+ const URL_PATTERN = /https?:\/\/[^\s<>"'),\]}]+/giu;
21
+
22
+ function sanitizeUrlForBom(value) {
23
+ const input = String(value || "").trim();
24
+ if (!input) {
25
+ return input;
26
+ }
27
+ try {
28
+ const parsed = new URL(input);
29
+ parsed.username = "";
30
+ parsed.password = "";
31
+ parsed.search = "";
32
+ parsed.hash = "";
33
+ return parsed.toString();
34
+ } catch {
35
+ return input;
36
+ }
37
+ }
38
+
39
+ function sanitizeTextForBom(value) {
40
+ let sanitized = String(value ?? "");
41
+ sanitized = sanitized.replace(URL_PATTERN, (match) =>
42
+ sanitizeUrlForBom(match),
43
+ );
44
+ for (const pattern of INLINE_CREDENTIAL_PATTERNS) {
45
+ sanitized = sanitized.replace(pattern, "[redacted]");
46
+ }
47
+ return sanitized;
48
+ }
49
+
50
+ function sanitizeStructuredValueForBom(value) {
51
+ if (typeof value === "string") {
52
+ return sanitizeTextForBom(value);
53
+ }
54
+ if (Array.isArray(value)) {
55
+ return value.map((entry) => sanitizeStructuredValueForBom(entry));
56
+ }
57
+ if (value && typeof value === "object") {
58
+ const sanitized = {};
59
+ for (const [key, entryValue] of Object.entries(value)) {
60
+ if (DANGEROUS_OBJECT_KEYS.has(key)) {
61
+ continue;
62
+ }
63
+ sanitized[key] = sanitizeStructuredValueForBom(entryValue);
64
+ }
65
+ return sanitized;
66
+ }
67
+ return value;
68
+ }
69
+
70
+ function extractCommandExecutable(command) {
71
+ const trimmedCommand = String(command || "").trim();
72
+ if (!trimmedCommand) {
73
+ return "";
74
+ }
75
+ const quotedMatch = trimmedCommand.match(/^(['"])(.*?)\1/u);
76
+ if (quotedMatch?.[2]) {
77
+ return quotedMatch[2];
78
+ }
79
+ const absolutePathMatch = trimmedCommand.match(
80
+ /^((?:[A-Za-z]:\\|\/).*?\.(?:bat|bin|cjs|cmd|com|exe|jar|js|mjs|ps1|py|rb|sh|ts|tsx))(?=\s|$)/iu,
81
+ );
82
+ if (absolutePathMatch?.[1]) {
83
+ return absolutePathMatch[1];
84
+ }
85
+ return trimmedCommand.split(/\s+/u)[0];
86
+ }
87
+
88
+ function summarizeExecutable(command) {
89
+ const executable = extractCommandExecutable(command);
90
+ if (!executable) {
91
+ return "configured";
92
+ }
93
+ if (executable.includes("\\")) {
94
+ return path.win32.basename(executable) || "configured";
95
+ }
96
+ return path.posix.basename(executable) || "configured";
97
+ }
98
+
99
+ export function sanitizeBomUrl(value) {
100
+ return sanitizeUrlForBom(value);
101
+ }
102
+
103
+ export function sanitizeBomPropertyValue(name, value) {
104
+ if (value === undefined || value === null || value === "") {
105
+ return value;
106
+ }
107
+ if (name === "cdx:mcp:command") {
108
+ const sanitizedCommand = sanitizeTextForBom(value).trim();
109
+ if (!sanitizedCommand) {
110
+ return sanitizedCommand;
111
+ }
112
+ return summarizeExecutable(sanitizedCommand);
113
+ }
114
+ if (JSON_PROPERTY_NAMES.has(name) || typeof value === "object") {
115
+ return JSON.stringify(sanitizeStructuredValueForBom(value));
116
+ }
117
+ if (typeof value === "string") {
118
+ return sanitizeTextForBom(value);
119
+ }
120
+ return value;
121
+ }
@@ -1,26 +1,151 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
- import { cdx_16, cdx_17 } from "@appthreat/cdx-proto";
4
3
  import {
5
- fromBinary,
6
- fromJsonString,
7
- toBinary,
8
- toJson,
9
- } from "@bufbuild/protobuf";
4
+ createBom,
5
+ decodeBomBinary,
6
+ decodeBomJson,
7
+ encodeBomBinary,
8
+ encodeBomJson,
9
+ parseBomBinary,
10
+ parseBomJson,
11
+ supportedSpecVersions,
12
+ } from "@appthreat/cdx-proto";
10
13
 
11
14
  import { safeExistsSync, safeWriteSync } from "./utils.js";
12
15
 
16
+ const JSON_READ_OPTIONS = {
17
+ ignoreUnknownFields: true,
18
+ };
19
+
20
+ const BINARY_READ_OPTIONS = {
21
+ readUnknownFields: true,
22
+ };
23
+
24
+ const BINARY_WRITE_OPTIONS = {
25
+ writeUnknownFields: true,
26
+ };
27
+
28
+ const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
29
+
30
+ const DEFAULT_SPEC_VERSION =
31
+ supportedSpecVersions[supportedSpecVersions.length - 1];
32
+
33
+ const isProtoMessageBom = (bom) =>
34
+ Boolean(
35
+ bom &&
36
+ typeof bom === "object" &&
37
+ !Array.isArray(bom) &&
38
+ typeof bom.$typeName === "string" &&
39
+ bom.specVersion,
40
+ );
41
+
42
+ const hasExplicitSpecVersion = (bomJson) =>
43
+ Boolean(
44
+ bomJson &&
45
+ typeof bomJson === "object" &&
46
+ !Array.isArray(bomJson) &&
47
+ (bomJson.specVersion !== undefined || bomJson.spec_version !== undefined),
48
+ );
49
+
50
+ const OBJECT_WRAPPED_LIST_FIELDS = ["declarations", "definitions"];
51
+
52
+ const isPlainObject = (value) =>
53
+ Boolean(value && typeof value === "object" && !Array.isArray(value));
54
+
55
+ const normalizeObjectWrappedListsForProto = (bomJson) => {
56
+ if (!isPlainObject(bomJson)) {
57
+ return bomJson;
58
+ }
59
+ const normalizedBomJson = { ...bomJson };
60
+ for (const fieldName of OBJECT_WRAPPED_LIST_FIELDS) {
61
+ if (isPlainObject(normalizedBomJson[fieldName])) {
62
+ normalizedBomJson[fieldName] = [normalizedBomJson[fieldName]];
63
+ }
64
+ }
65
+ return normalizedBomJson;
66
+ };
67
+
68
+ const mergeObjectWrappedListEntries = (entries) => {
69
+ const mergedEntry = {};
70
+ for (const entry of entries) {
71
+ if (!isPlainObject(entry)) {
72
+ continue;
73
+ }
74
+ for (const [key, value] of Object.entries(entry)) {
75
+ if (value === undefined) {
76
+ continue;
77
+ }
78
+ if (Array.isArray(value)) {
79
+ mergedEntry[key] = [...(mergedEntry[key] || []), ...value];
80
+ continue;
81
+ }
82
+ if (isPlainObject(value) && isPlainObject(mergedEntry[key])) {
83
+ mergedEntry[key] = { ...mergedEntry[key], ...value };
84
+ continue;
85
+ }
86
+ if (mergedEntry[key] === undefined) {
87
+ mergedEntry[key] = value;
88
+ }
89
+ }
90
+ }
91
+ return Object.keys(mergedEntry).length ? mergedEntry : undefined;
92
+ };
93
+
94
+ const normalizeObjectWrappedListsFromProto = (bomJson) => {
95
+ if (!isPlainObject(bomJson)) {
96
+ return bomJson;
97
+ }
98
+ const normalizedBomJson = { ...bomJson };
99
+ for (const fieldName of OBJECT_WRAPPED_LIST_FIELDS) {
100
+ if (!Array.isArray(normalizedBomJson[fieldName])) {
101
+ continue;
102
+ }
103
+ const mergedEntry = mergeObjectWrappedListEntries(
104
+ normalizedBomJson[fieldName],
105
+ );
106
+ if (mergedEntry) {
107
+ normalizedBomJson[fieldName] = mergedEntry;
108
+ } else {
109
+ delete normalizedBomJson[fieldName];
110
+ }
111
+ }
112
+ return normalizedBomJson;
113
+ };
114
+
115
+ const resolveBomMessage = (bomJson, specVersion = DEFAULT_SPEC_VERSION) => {
116
+ if (isProtoMessageBom(bomJson)) {
117
+ return bomJson;
118
+ }
119
+ if (typeof bomJson === "string" || bomJson instanceof String) {
120
+ const parsedBomJson = normalizeObjectWrappedListsForProto(
121
+ JSON.parse(`${bomJson}`),
122
+ );
123
+ if (hasExplicitSpecVersion(parsedBomJson)) {
124
+ return parseBomJson(parsedBomJson, JSON_READ_OPTIONS);
125
+ }
126
+ return decodeBomJson(specVersion, parsedBomJson, JSON_READ_OPTIONS);
127
+ }
128
+ if (bomJson && typeof bomJson === "object" && !Array.isArray(bomJson)) {
129
+ const normalizedBomJson = normalizeObjectWrappedListsForProto(bomJson);
130
+ if (hasExplicitSpecVersion(normalizedBomJson)) {
131
+ return parseBomJson(normalizedBomJson, JSON_READ_OPTIONS);
132
+ }
133
+ return decodeBomJson(specVersion, normalizedBomJson, JSON_READ_OPTIONS);
134
+ }
135
+ return createBom(specVersion);
136
+ };
137
+
13
138
  /**
14
- * Stringify the given bom json based on the type.
139
+ * Determine whether a path looks like a CycloneDX protobuf file.
15
140
  *
16
- * @param {string | Object} bomJson string or object
17
- * @returns {string} BOM json string
141
+ * @param {string} filePath File path
142
+ * @returns {boolean} true when the path looks like a protobuf BOM file
18
143
  */
19
- const stringifyIfNeeded = (bomJson) => {
20
- if (typeof bomJson === "string" || bomJson instanceof String) {
21
- return bomJson;
22
- }
23
- return JSON.stringify(bomJson);
144
+ export const isProtoBomFile = (filePath) => {
145
+ const normalizedPath = `${filePath || ""}`.toLowerCase();
146
+ return PROTO_BOM_FILE_EXTENSIONS.some((extension) =>
147
+ normalizedPath.endsWith(extension),
148
+ );
24
149
  };
25
150
 
26
151
  /**
@@ -28,27 +153,16 @@ const stringifyIfNeeded = (bomJson) => {
28
153
  *
29
154
  * @param {string | Object} bomJson BOM Json
30
155
  * @param {string} binFile Binary file name
156
+ * @param {string | number} [specVersion] CycloneDX spec version fallback for BOMs without specVersion
31
157
  */
32
- export const writeBinary = (bomJson, binFile) => {
158
+ export const writeBinary = (
159
+ bomJson,
160
+ binFile,
161
+ specVersion = DEFAULT_SPEC_VERSION,
162
+ ) => {
33
163
  if (bomJson && binFile) {
34
- let bomSchema;
35
- if (+bomJson.specVersion === 1.7) {
36
- bomSchema = cdx_17.BomSchema;
37
- } else {
38
- bomSchema = cdx_16.BomSchema;
39
- }
40
- safeWriteSync(
41
- binFile,
42
- toBinary(
43
- bomSchema,
44
- fromJsonString(bomSchema, stringifyIfNeeded(bomJson), {
45
- ignoreUnknownFields: true,
46
- }),
47
- ),
48
- {
49
- writeUnknownFields: true,
50
- },
51
- );
164
+ const bomMessage = resolveBomMessage(bomJson, specVersion);
165
+ safeWriteSync(binFile, encodeBomBinary(bomMessage, BINARY_WRITE_OPTIONS));
52
166
  }
53
167
  };
54
168
 
@@ -57,23 +171,20 @@ export const writeBinary = (bomJson, binFile) => {
57
171
  *
58
172
  * @param {string} binFile Binary file name
59
173
  * @param {boolean} asJson Convert to JSON
60
- * @param {number} specVersion Specification version. Defaults to 1.7
174
+ * @param {string | number} [specVersion] Optional specification version. When omitted, cdxgen auto-detects the matching schema.
61
175
  */
62
- export const readBinary = (binFile, asJson = true, specVersion = 1.7) => {
176
+ export const readBinary = (binFile, asJson, specVersion) => {
177
+ asJson = asJson ?? true;
63
178
  if (!safeExistsSync(binFile)) {
64
179
  return undefined;
65
180
  }
66
- let bomSchema;
67
- if (specVersion === 1.7) {
68
- bomSchema = cdx_17.BomSchema;
69
- } else {
70
- bomSchema = cdx_16.BomSchema;
71
- }
72
- const bomObject = fromBinary(bomSchema, readFileSync(binFile), {
73
- readUnknownFields: true,
74
- });
181
+ const binaryData = readFileSync(binFile);
182
+ const bomObject =
183
+ specVersion !== undefined && specVersion !== null && specVersion !== ""
184
+ ? decodeBomBinary(specVersion, binaryData, BINARY_READ_OPTIONS)
185
+ : parseBomBinary(binaryData, BINARY_READ_OPTIONS);
75
186
  if (asJson) {
76
- return toJson(bomSchema, bomObject, { emitDefaultValues: true });
187
+ return normalizeObjectWrappedListsFromProto(encodeBomJson(bomObject));
77
188
  }
78
189
  return bomObject;
79
190
  };
@@ -3,27 +3,46 @@ import { join } from "node:path";
3
3
 
4
4
  import { assert, it } from "poku";
5
5
 
6
- import { readBinary, writeBinary } from "./protobom.js";
6
+ import { isProtoBomFile, readBinary, writeBinary } from "./protobom.js";
7
7
  import { getTmpDir } from "./utils.js";
8
8
 
9
- const tempDir = mkdtempSync(join(getTmpDir(), "bin-tests-"));
10
9
  const testBom = JSON.parse(
11
10
  readFileSync("./test/data/bom-java.json", { encoding: "utf-8" }),
12
11
  );
12
+ const cbomFixture = JSON.parse(
13
+ readFileSync("./test/data/bom-cbom-js-fixture.json", { encoding: "utf-8" }),
14
+ );
15
+
16
+ const createTempDir = () => mkdtempSync(join(getTmpDir(), "bin-tests-"));
17
+
18
+ const cleanupTempDir = (tempDir) => {
19
+ if (tempDir?.startsWith(getTmpDir()) && rmSync) {
20
+ rmSync(tempDir, { recursive: true, force: true });
21
+ }
22
+ };
13
23
 
14
24
  it("proto binary tests", () => {
25
+ const tempDir = createTempDir();
15
26
  const binFile = join(tempDir, "test.cdx.bin");
16
27
  writeBinary({}, binFile);
17
28
  assert.deepStrictEqual(existsSync(binFile), true);
18
29
  writeBinary(testBom, binFile);
19
30
  assert.deepStrictEqual(existsSync(binFile), true);
31
+ assert.equal(isProtoBomFile(binFile), true);
32
+ assert.equal(isProtoBomFile("test.proto"), true);
33
+ assert.equal(isProtoBomFile("bom.json"), false);
20
34
  let bomObject = readBinary(binFile);
21
35
  assert.ok(bomObject);
22
36
  assert.deepStrictEqual(
23
37
  bomObject.serialNumber,
24
38
  "urn:uuid:cc8b5a04-2698-4375-b04c-cedfa4317fee",
25
39
  );
40
+ assert.deepStrictEqual(bomObject.bomFormat, "CycloneDX");
26
41
  assert.deepStrictEqual(bomObject.specVersion, "1.5");
42
+ assert.equal(
43
+ bomObject.metadata.component.type.startsWith("CLASSIFICATION_"),
44
+ false,
45
+ );
27
46
  bomObject = readBinary(binFile, false, 1.5);
28
47
  assert.ok(bomObject);
29
48
  assert.deepStrictEqual(
@@ -31,7 +50,123 @@ it("proto binary tests", () => {
31
50
  "urn:uuid:cc8b5a04-2698-4375-b04c-cedfa4317fee",
32
51
  );
33
52
  assert.deepStrictEqual(bomObject.specVersion, "1.5");
34
- if (tempDir?.startsWith(getTmpDir()) && rmSync) {
35
- rmSync(tempDir, { recursive: true, force: true });
36
- }
53
+ const modernBinFile = join(tempDir, "test-1.7.cdx");
54
+ writeBinary(
55
+ {
56
+ bomFormat: "CycloneDX",
57
+ metadata: {
58
+ component: {
59
+ name: "cdxgen",
60
+ type: "application",
61
+ },
62
+ },
63
+ serialNumber: "urn:uuid:11111111-1111-1111-1111-111111111111",
64
+ specVersion: "1.7",
65
+ version: 1,
66
+ },
67
+ modernBinFile,
68
+ );
69
+ const modernBomObject = readBinary(modernBinFile);
70
+ assert.ok(modernBomObject);
71
+ assert.deepStrictEqual(modernBomObject.bomFormat, "CycloneDX");
72
+ assert.deepStrictEqual(modernBomObject.specVersion, "1.7");
73
+ assert.deepStrictEqual(
74
+ modernBomObject.metadata.component.type,
75
+ "application",
76
+ );
77
+ assert.deepStrictEqual(modernBomObject.metadata.component.name, "cdxgen");
78
+ cleanupTempDir(tempDir);
79
+ });
80
+
81
+ it("keeps canonical definitions and declarations as objects during proto round-trip", () => {
82
+ const tempDir = createTempDir();
83
+ const binFile = join(tempDir, "standard-sections.cdx");
84
+ writeBinary(
85
+ {
86
+ bomFormat: "CycloneDX",
87
+ declarations: {
88
+ affirmation: {
89
+ statement: "verified",
90
+ },
91
+ claims: [
92
+ {
93
+ predicate: "meets-control",
94
+ target: "pkg:npm/demo-app@1.0.0",
95
+ },
96
+ ],
97
+ },
98
+ definitions: {
99
+ standards: [
100
+ {
101
+ name: "ASVS",
102
+ requirements: [
103
+ {
104
+ identifier: "V1.1",
105
+ title: "Authenticate requests",
106
+ },
107
+ ],
108
+ version: "5.0",
109
+ },
110
+ ],
111
+ },
112
+ metadata: {
113
+ component: {
114
+ name: "demo-app",
115
+ type: "application",
116
+ version: "1.0.0",
117
+ },
118
+ },
119
+ serialNumber: "urn:uuid:22222222-2222-2222-2222-222222222222",
120
+ specVersion: "1.7",
121
+ version: 1,
122
+ },
123
+ binFile,
124
+ );
125
+
126
+ const bomObject = readBinary(binFile);
127
+ assert.ok(bomObject);
128
+ assert.equal(Array.isArray(bomObject.definitions), false);
129
+ assert.equal(Array.isArray(bomObject.declarations), false);
130
+ assert.equal(bomObject.definitions.standards[0].name, "ASVS");
131
+ assert.equal(
132
+ bomObject.definitions.standards[0].requirements[0].identifier,
133
+ "V1.1",
134
+ );
135
+ assert.equal(bomObject.declarations.claims[0].predicate, "meets-control");
136
+ assert.equal(bomObject.declarations.affirmation.statement, "verified");
137
+ cleanupTempDir(tempDir);
138
+ });
139
+
140
+ it("round-trips real CBOM fixture data with cryptographic assets intact", () => {
141
+ const tempDir = createTempDir();
142
+ const binFile = join(tempDir, "cbom-fixture.cdx");
143
+ writeBinary(cbomFixture, binFile);
144
+
145
+ const bomObject = readBinary(binFile);
146
+ const cryptoComponents = (bomObject.components || []).filter(
147
+ (component) => component.type === "cryptographic-asset",
148
+ );
149
+
150
+ assert.ok(bomObject);
151
+ assert.equal(bomObject.specVersion, "1.7");
152
+ assert.ok(cryptoComponents.length >= 3);
153
+ assert.equal(
154
+ cryptoComponents.some(
155
+ (component) => component.cryptoProperties?.assetType === "algorithm",
156
+ ),
157
+ true,
158
+ );
159
+ assert.equal(
160
+ cryptoComponents.some((component) => component.purl !== undefined),
161
+ false,
162
+ );
163
+ assert.equal(
164
+ cryptoComponents.some(
165
+ (component) =>
166
+ component.name === "sha-512" &&
167
+ component.cryptoProperties?.oid === "2.16.840.1.101.3.4.2.3",
168
+ ),
169
+ true,
170
+ );
171
+ cleanupTempDir(tempDir);
37
172
  });
@@ -1,13 +1,46 @@
1
1
  import { Buffer } from "node:buffer";
2
2
 
3
+ import { hasDangerousUnicode } from "../utils.js";
4
+
5
+ /**
6
+ * Returns the Dependency-Track BOM API URL as a sanitized URL object.
7
+ *
8
+ * @param {string} serverUrl Dependency-Track server URL
9
+ * @returns {URL | undefined} API URL to submit BOM payload
10
+ */
11
+ export function getDependencyTrackBomApiUrl(serverUrl) {
12
+ const rawServerUrl = `${serverUrl || ""}`.trim();
13
+ if (!rawServerUrl || hasDangerousUnicode(rawServerUrl)) {
14
+ return undefined;
15
+ }
16
+ let parsedUrl;
17
+ try {
18
+ parsedUrl = new URL(rawServerUrl);
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
23
+ return undefined;
24
+ }
25
+ if (!parsedUrl.hostname || hasDangerousUnicode(parsedUrl.hostname)) {
26
+ return undefined;
27
+ }
28
+ parsedUrl.username = "";
29
+ parsedUrl.password = "";
30
+ parsedUrl.search = "";
31
+ parsedUrl.hash = "";
32
+ parsedUrl.pathname = `${parsedUrl.pathname.replace(/\/+$/, "")}/api/v1/bom`;
33
+ return parsedUrl;
34
+ }
35
+
3
36
  /**
4
- * Returns the Dependency-Track BOM API URL.
37
+ * Returns the Dependency-Track BOM API URL string.
5
38
  *
6
39
  * @param {string} serverUrl Dependency-Track server URL
7
- * @returns {string} API URL to submit BOM payload
40
+ * @returns {string | undefined} API URL to submit BOM payload
8
41
  */
9
42
  export function getDependencyTrackBomUrl(serverUrl) {
10
- return `${serverUrl.replace(/\/$/, "")}/api/v1/bom`;
43
+ return getDependencyTrackBomApiUrl(serverUrl)?.toString();
11
44
  }
12
45
 
13
46
  /**
@@ -2,6 +2,7 @@ import { assert, describe, it } from "poku";
2
2
 
3
3
  import {
4
4
  buildDependencyTrackBomPayload,
5
+ getDependencyTrackBomApiUrl,
5
6
  getDependencyTrackBomUrl,
6
7
  } from "./dependency-track.js";
7
8
 
@@ -17,6 +18,49 @@ describe("Dependency-Track helper tests", () => {
17
18
  );
18
19
  });
19
20
 
21
+ it("removes credentials, query strings, and fragments from the submission URL", () => {
22
+ assert.strictEqual(
23
+ getDependencyTrackBomUrl(
24
+ "https://user:pass@dtrack.example.com/base/?token=secret#frag",
25
+ ),
26
+ "https://dtrack.example.com/base/api/v1/bom",
27
+ );
28
+ });
29
+
30
+ it("returns a sanitized URL object for Dependency-Track requests", () => {
31
+ const apiUrl = getDependencyTrackBomApiUrl(
32
+ "https://user:pass@dtrack.example.com/base/?token=secret#frag",
33
+ );
34
+ assert.ok(apiUrl instanceof URL);
35
+ assert.strictEqual(apiUrl?.hostname, "dtrack.example.com");
36
+ assert.strictEqual(apiUrl?.pathname, "/base/api/v1/bom");
37
+ assert.strictEqual(apiUrl?.username, "");
38
+ assert.strictEqual(apiUrl?.password, "");
39
+ assert.strictEqual(apiUrl?.search, "");
40
+ assert.strictEqual(apiUrl?.hash, "");
41
+ });
42
+
43
+ it("rejects malformed or unsupported submission URLs", () => {
44
+ assert.strictEqual(
45
+ getDependencyTrackBomUrl("file:///tmp/dtrack"),
46
+ undefined,
47
+ );
48
+ assert.strictEqual(
49
+ getDependencyTrackBomApiUrl("file:///tmp/dtrack"),
50
+ undefined,
51
+ );
52
+ assert.strictEqual(
53
+ getDependencyTrackBomUrl("javascript:alert(1)"),
54
+ undefined,
55
+ );
56
+ assert.strictEqual(
57
+ getDependencyTrackBomApiUrl("javascript:alert(1)"),
58
+ undefined,
59
+ );
60
+ assert.strictEqual(getDependencyTrackBomUrl("not a url"), undefined);
61
+ assert.strictEqual(getDependencyTrackBomApiUrl("not a url"), undefined);
62
+ });
63
+
20
64
  it("builds payload with parentUUID and tags", () => {
21
65
  const payload = buildDependencyTrackBomPayload(
22
66
  {
@@ -77,6 +77,21 @@ function isSafeGitRefName(refName) {
77
77
  return /^[A-Za-z0-9._/@+-]+$/.test(refName);
78
78
  }
79
79
 
80
+ function inferGitOperation(args = []) {
81
+ for (let index = 0; index < args.length; index += 1) {
82
+ const arg = args[index];
83
+ if (arg === "-c" || arg === "--config-env") {
84
+ index += 1;
85
+ continue;
86
+ }
87
+ if (typeof arg === "string" && arg.startsWith("-")) {
88
+ continue;
89
+ }
90
+ return arg || "command";
91
+ }
92
+ return "command";
93
+ }
94
+
80
95
  /**
81
96
  * Execute git with hardened defaults.
82
97
  *
@@ -86,6 +101,7 @@ function isSafeGitRefName(refName) {
86
101
  * @returns {Object} spawn result
87
102
  */
88
103
  export function hardenedGitCommand(args, options = {}) {
104
+ const gitOperation = inferGitOperation(args);
89
105
  const gitAllowProtocol = getGitAllowProtocol();
90
106
  const envConfigs = {
91
107
  GIT_CONFIG_COUNT: "2",
@@ -109,6 +125,14 @@ export function hardenedGitCommand(args, options = {}) {
109
125
  GIT_ALLOW_PROTOCOL: gitAllowProtocol,
110
126
  };
111
127
  return safeSpawnSync("git", args, {
128
+ cdxgenActivity: {
129
+ blockedReason: `Dry run mode blocks git ${gitOperation} operations.`,
130
+ gitOperation,
131
+ kind: `git-${gitOperation}`,
132
+ metadata: {
133
+ capability: "git-operation",
134
+ },
135
+ },
112
136
  shell: false,
113
137
  cwd: options.cwd,
114
138
  env,