@cyclonedx/cdxgen 12.2.1 → 12.3.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 (170) hide show
  1. package/README.md +239 -90
  2. package/bin/audit.js +191 -0
  3. package/bin/cdxgen.js +513 -167
  4. package/bin/convert.js +99 -0
  5. package/bin/evinse.js +23 -0
  6. package/bin/repl.js +339 -8
  7. package/bin/sign.js +8 -0
  8. package/bin/validate.js +8 -0
  9. package/bin/verify.js +8 -0
  10. package/data/container-knowledge-index.json +125 -0
  11. package/data/gtfobins-index.json +6296 -0
  12. package/data/lolbas-index.json +150 -0
  13. package/data/queries-darwin.json +63 -3
  14. package/data/queries-win.json +45 -3
  15. package/data/queries.json +74 -2
  16. package/data/rules/chrome-extensions.yaml +240 -0
  17. package/data/rules/ci-permissions.yaml +478 -18
  18. package/data/rules/container-risk.yaml +270 -0
  19. package/data/rules/obom-runtime.yaml +891 -0
  20. package/data/rules/package-integrity.yaml +49 -0
  21. package/data/spdx-export.schema.json +6794 -0
  22. package/data/spdx-model-v3.0.1.jsonld +15999 -0
  23. package/lib/audit/index.js +1924 -0
  24. package/lib/audit/index.poku.js +1488 -0
  25. package/lib/audit/progress.js +137 -0
  26. package/lib/audit/progress.poku.js +188 -0
  27. package/lib/audit/reporters.js +618 -0
  28. package/lib/audit/scoring.js +310 -0
  29. package/lib/audit/scoring.poku.js +341 -0
  30. package/lib/audit/targets.js +260 -0
  31. package/lib/audit/targets.poku.js +331 -0
  32. package/lib/cli/index.js +154 -11
  33. package/lib/cli/index.poku.js +251 -0
  34. package/lib/helpers/analyzer.js +446 -2
  35. package/lib/helpers/analyzer.poku.js +72 -1
  36. package/lib/helpers/annotationFormatter.js +49 -0
  37. package/lib/helpers/annotationFormatter.poku.js +44 -0
  38. package/lib/helpers/bomUtils.js +36 -0
  39. package/lib/helpers/bomUtils.poku.js +51 -0
  40. package/lib/helpers/caxa.js +2 -2
  41. package/lib/helpers/chromextutils.js +1153 -0
  42. package/lib/helpers/chromextutils.poku.js +493 -0
  43. package/lib/helpers/ciParsers/githubActions.js +1632 -45
  44. package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
  45. package/lib/helpers/containerRisk.js +186 -0
  46. package/lib/helpers/containerRisk.poku.js +52 -0
  47. package/lib/helpers/display.js +241 -59
  48. package/lib/helpers/display.poku.js +162 -2
  49. package/lib/helpers/exportUtils.js +123 -0
  50. package/lib/helpers/exportUtils.poku.js +60 -0
  51. package/lib/helpers/formulationParsers.js +69 -0
  52. package/lib/helpers/formulationParsers.poku.js +44 -0
  53. package/lib/helpers/gtfobins.js +189 -0
  54. package/lib/helpers/gtfobins.poku.js +49 -0
  55. package/lib/helpers/lolbas.js +267 -0
  56. package/lib/helpers/lolbas.poku.js +39 -0
  57. package/lib/helpers/osqueryTransform.js +84 -0
  58. package/lib/helpers/osqueryTransform.poku.js +49 -0
  59. package/lib/helpers/provenanceUtils.js +193 -0
  60. package/lib/helpers/provenanceUtils.poku.js +145 -0
  61. package/lib/helpers/pylockutils.js +281 -0
  62. package/lib/helpers/pylockutils.poku.js +48 -0
  63. package/lib/helpers/registryProvenance.js +793 -0
  64. package/lib/helpers/registryProvenance.poku.js +452 -0
  65. package/lib/helpers/source.js +1267 -0
  66. package/lib/helpers/source.poku.js +771 -0
  67. package/lib/helpers/spdxUtils.js +97 -0
  68. package/lib/helpers/spdxUtils.poku.js +70 -0
  69. package/lib/helpers/unicodeScan.js +147 -0
  70. package/lib/helpers/unicodeScan.poku.js +45 -0
  71. package/lib/helpers/utils.js +700 -128
  72. package/lib/helpers/utils.poku.js +877 -80
  73. package/lib/managers/binary.js +29 -5
  74. package/lib/managers/docker.js +179 -52
  75. package/lib/managers/docker.poku.js +327 -28
  76. package/lib/managers/oci.js +107 -23
  77. package/lib/managers/oci.poku.js +132 -0
  78. package/lib/server/openapi.yaml +17 -0
  79. package/lib/server/server.js +225 -336
  80. package/lib/server/server.poku.js +16 -10
  81. package/lib/stages/postgen/annotator.js +7 -0
  82. package/lib/stages/postgen/annotator.poku.js +40 -0
  83. package/lib/stages/postgen/auditBom.js +19 -3
  84. package/lib/stages/postgen/auditBom.poku.js +1729 -67
  85. package/lib/stages/postgen/postgen.js +40 -0
  86. package/lib/stages/postgen/postgen.poku.js +47 -0
  87. package/lib/stages/postgen/ruleEngine.js +80 -2
  88. package/lib/stages/postgen/spdxConverter.js +796 -0
  89. package/lib/stages/postgen/spdxConverter.poku.js +341 -0
  90. package/lib/validator/bomValidator.js +232 -0
  91. package/lib/validator/bomValidator.poku.js +70 -0
  92. package/lib/validator/complianceRules.js +70 -7
  93. package/lib/validator/complianceRules.poku.js +30 -0
  94. package/lib/validator/reporters/annotations.js +2 -2
  95. package/lib/validator/reporters/console.js +11 -0
  96. package/lib/validator/reporters.poku.js +13 -0
  97. package/package.json +10 -7
  98. package/types/bin/audit.d.ts +3 -0
  99. package/types/bin/audit.d.ts.map +1 -0
  100. package/types/bin/convert.d.ts +3 -0
  101. package/types/bin/convert.d.ts.map +1 -0
  102. package/types/bin/repl.d.ts.map +1 -1
  103. package/types/lib/audit/index.d.ts +115 -0
  104. package/types/lib/audit/index.d.ts.map +1 -0
  105. package/types/lib/audit/progress.d.ts +27 -0
  106. package/types/lib/audit/progress.d.ts.map +1 -0
  107. package/types/lib/audit/reporters.d.ts +35 -0
  108. package/types/lib/audit/reporters.d.ts.map +1 -0
  109. package/types/lib/audit/scoring.d.ts +35 -0
  110. package/types/lib/audit/scoring.d.ts.map +1 -0
  111. package/types/lib/audit/targets.d.ts +63 -0
  112. package/types/lib/audit/targets.d.ts.map +1 -0
  113. package/types/lib/cli/index.d.ts +8 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/helpers/analyzer.d.ts +13 -0
  116. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  117. package/types/lib/helpers/annotationFormatter.d.ts +23 -0
  118. package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
  119. package/types/lib/helpers/bomUtils.d.ts +5 -0
  120. package/types/lib/helpers/bomUtils.d.ts.map +1 -0
  121. package/types/lib/helpers/chromextutils.d.ts +97 -0
  122. package/types/lib/helpers/chromextutils.d.ts.map +1 -0
  123. package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
  124. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  125. package/types/lib/helpers/containerRisk.d.ts +17 -0
  126. package/types/lib/helpers/containerRisk.d.ts.map +1 -0
  127. package/types/lib/helpers/display.d.ts +4 -1
  128. package/types/lib/helpers/display.d.ts.map +1 -1
  129. package/types/lib/helpers/exportUtils.d.ts +40 -0
  130. package/types/lib/helpers/exportUtils.d.ts.map +1 -0
  131. package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
  132. package/types/lib/helpers/gtfobins.d.ts +17 -0
  133. package/types/lib/helpers/gtfobins.d.ts.map +1 -0
  134. package/types/lib/helpers/lolbas.d.ts +16 -0
  135. package/types/lib/helpers/lolbas.d.ts.map +1 -0
  136. package/types/lib/helpers/osqueryTransform.d.ts +7 -0
  137. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
  138. package/types/lib/helpers/provenanceUtils.d.ts +90 -0
  139. package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
  140. package/types/lib/helpers/pylockutils.d.ts +51 -0
  141. package/types/lib/helpers/pylockutils.d.ts.map +1 -0
  142. package/types/lib/helpers/registryProvenance.d.ts +17 -0
  143. package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
  144. package/types/lib/helpers/source.d.ts +141 -0
  145. package/types/lib/helpers/source.d.ts.map +1 -0
  146. package/types/lib/helpers/spdxUtils.d.ts +2 -0
  147. package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
  148. package/types/lib/helpers/unicodeScan.d.ts +46 -0
  149. package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
  150. package/types/lib/helpers/utils.d.ts +29 -11
  151. package/types/lib/helpers/utils.d.ts.map +1 -1
  152. package/types/lib/managers/binary.d.ts.map +1 -1
  153. package/types/lib/managers/docker.d.ts.map +1 -1
  154. package/types/lib/managers/oci.d.ts.map +1 -1
  155. package/types/lib/server/server.d.ts +0 -36
  156. package/types/lib/server/server.d.ts.map +1 -1
  157. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  158. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  159. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  160. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  161. package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
  162. package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
  163. package/types/lib/validator/bomValidator.d.ts +1 -0
  164. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  165. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  166. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  167. package/types/bin/dependencies.d.ts +0 -3
  168. package/types/bin/dependencies.d.ts.map +0 -1
  169. package/types/bin/licenses.d.ts +0 -3
  170. package/types/bin/licenses.d.ts.map +0 -1
@@ -889,13 +889,340 @@ const getAllSrcJSAndTSFiles = (src, deep) =>
889
889
  getAllFiles(deep, src, ".svelte"),
890
890
  ]);
891
891
 
892
+ export const CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES = [
893
+ "fileAccess",
894
+ "deviceAccess",
895
+ "network",
896
+ "bluetooth",
897
+ "accessibility",
898
+ "codeInjection",
899
+ "fingerprinting",
900
+ ];
901
+
902
+ const EXTENSION_CAPABILITY_CHAIN_PATTERNS = {
903
+ fileAccess: [
904
+ /^(chrome|browser)\.(downloads|fileSystem|fileBrowserHandler|fileManagerPrivate)\b/i,
905
+ /^(window\.)?show(Open|Save|Directory)FilePicker$/i,
906
+ ],
907
+ deviceAccess: [
908
+ /^(chrome|browser)\.(usb|hid|serial|nfc|mediaGalleries|gcdPrivate|bluetooth|bluetoothPrivate)\b/i,
909
+ ],
910
+ network: [
911
+ /^(chrome|browser)\.(webRequest|declarativeNetRequest|proxy|webNavigation|socket)\b/i,
912
+ /^(window\.)?(fetch|WebSocket|EventSource)$/i,
913
+ /^(XMLHttpRequest)\b/i,
914
+ /^navigator\.sendBeacon$/i,
915
+ ],
916
+ bluetooth: [/^(chrome|browser)\.(bluetooth|bluetoothPrivate)\b/i],
917
+ accessibility: [
918
+ /^(chrome|browser)\.(accessibilityFeatures|accessibilityPrivate|automation)\b/i,
919
+ ],
920
+ codeInjection: [
921
+ /^(chrome|browser)\.(scripting\.executeScript|tabs\.executeScript|userScripts|debugger)\b/i,
922
+ /^(window\.)?(eval|Function)$/i,
923
+ /^document\.write$/i,
924
+ ],
925
+ fingerprinting: [
926
+ /^navigator\.(userAgent|platform|languages|language|hardwareConcurrency|deviceMemory|plugins|userAgentData)\b/i,
927
+ /^(screen\.)?(width|height|availWidth|availHeight|colorDepth|pixelDepth)$/i,
928
+ /^(window\.)?(AudioContext|OfflineAudioContext|RTCPeerConnection)$/i,
929
+ /^(canvas|[a-zA-Z_$][a-zA-Z0-9_$]*\.(getImageData|toDataURL|measureText))$/i,
930
+ ],
931
+ };
932
+
933
+ const EXTENSION_CAPABILITY_IDENTIFIER_PATTERNS = {
934
+ network: /^(fetch|WebSocket|EventSource|XMLHttpRequest)$/i,
935
+ codeInjection: /^(eval|Function)$/i,
936
+ fingerprinting: /^(AudioContext|OfflineAudioContext|RTCPeerConnection)$/i,
937
+ };
938
+
939
+ const SUSPICIOUS_JS_PROCESS_MODULES = new Set([
940
+ "child_process",
941
+ "node:child_process",
942
+ ]);
943
+
944
+ const SUSPICIOUS_JS_NETWORK_MODULES = new Set([
945
+ "axios",
946
+ "got",
947
+ "http",
948
+ "https",
949
+ "net",
950
+ "node-fetch",
951
+ "node:http",
952
+ "node:https",
953
+ "node:net",
954
+ "node:tls",
955
+ "tls",
956
+ "undici",
957
+ ]);
958
+
959
+ const SUSPICIOUS_JS_EXECUTION_MEMBERS = new Set([
960
+ "exec",
961
+ "execFile",
962
+ "execFileSync",
963
+ "execSync",
964
+ "fork",
965
+ "spawn",
966
+ "spawnSync",
967
+ ]);
968
+
969
+ const SUSPICIOUS_JS_NETWORK_MEMBERS = new Set([
970
+ "fetch",
971
+ "get",
972
+ "post",
973
+ "put",
974
+ "patch",
975
+ "request",
976
+ ]);
977
+
978
+ const SUSPICIOUS_JS_LONG_BASE64_PATTERN = /\b[A-Za-z0-9+/]{80,}={0,2}\b/;
979
+
980
+ const getLiteralStringValue = (node) => {
981
+ if (!node) {
982
+ return undefined;
983
+ }
984
+ if (node.type === "StringLiteral") {
985
+ return node.value;
986
+ }
987
+ if (node.type === "TemplateLiteral" && node.expressions?.length === 0) {
988
+ return node.quasis.map((quasi) => quasi.value.cooked || "").join("");
989
+ }
990
+ return undefined;
991
+ };
992
+
993
+ const addSuspiciousLiteralIndicators = (obfuscationIndicators, rawValue) => {
994
+ if (!rawValue || typeof rawValue !== "string") {
995
+ return;
996
+ }
997
+ if (SUSPICIOUS_JS_LONG_BASE64_PATTERN.test(rawValue)) {
998
+ obfuscationIndicators.add("long-base64-literal");
999
+ }
1000
+ };
1001
+
1002
+ const trackSuspiciousModuleReference = (
1003
+ moduleName,
1004
+ localName,
1005
+ executionIndicators,
1006
+ networkIndicators,
1007
+ processAliases,
1008
+ networkAliases,
1009
+ ) => {
1010
+ if (!moduleName || typeof moduleName !== "string") {
1011
+ return;
1012
+ }
1013
+ if (SUSPICIOUS_JS_PROCESS_MODULES.has(moduleName)) {
1014
+ executionIndicators.add("child-process-import");
1015
+ if (localName) {
1016
+ processAliases.add(localName);
1017
+ }
1018
+ }
1019
+ if (SUSPICIOUS_JS_NETWORK_MODULES.has(moduleName)) {
1020
+ networkIndicators.add("network-module-import");
1021
+ if (localName) {
1022
+ networkAliases.add(localName);
1023
+ }
1024
+ }
1025
+ };
1026
+
1027
+ const getMemberChainString = (node) => {
1028
+ if (!node) {
1029
+ return "";
1030
+ }
1031
+ if (node.type === "Identifier") {
1032
+ return node.name;
1033
+ }
1034
+ if (node.type === "ThisExpression") {
1035
+ return "this";
1036
+ }
1037
+ if (node.type === "StringLiteral") {
1038
+ return node.value;
1039
+ }
1040
+ if (node.type === "MetaProperty") {
1041
+ const metaName = node.meta?.name || "";
1042
+ const propertyName = node.property?.name || "";
1043
+ return [metaName, propertyName].filter(Boolean).join(".");
1044
+ }
1045
+ if (node.type === "CallExpression") {
1046
+ return getMemberChainString(node.callee);
1047
+ }
1048
+ if (node.type === "OptionalCallExpression") {
1049
+ return getMemberChainString(node.callee);
1050
+ }
1051
+ if (
1052
+ node.type !== "MemberExpression" &&
1053
+ node.type !== "OptionalMemberExpression"
1054
+ ) {
1055
+ return "";
1056
+ }
1057
+ const objectChain = getMemberChainString(node.object);
1058
+ const propertyChain = getMemberChainString(node.property);
1059
+ if (objectChain && propertyChain) {
1060
+ return `${objectChain}.${propertyChain}`;
1061
+ }
1062
+ return objectChain || propertyChain || "";
1063
+ };
1064
+
1065
+ function analyzeSuspiciousJsSource(source) {
1066
+ const executionIndicators = new Set();
1067
+ const networkIndicators = new Set();
1068
+ const obfuscationIndicators = new Set();
1069
+ const processAliases = new Set();
1070
+ const networkAliases = new Set();
1071
+ let ast;
1072
+ try {
1073
+ ast = parse(source, babelParserOptions);
1074
+ } catch {
1075
+ return {
1076
+ executionIndicators: [],
1077
+ indicators: [],
1078
+ networkIndicators: [],
1079
+ obfuscationIndicators: [],
1080
+ };
1081
+ }
1082
+ traverse.default(ast, {
1083
+ ImportDeclaration: (path) => {
1084
+ const moduleName = getLiteralStringValue(path?.node?.source);
1085
+ path.node.specifiers.forEach((specifier) => {
1086
+ trackSuspiciousModuleReference(
1087
+ moduleName,
1088
+ specifier?.local?.name,
1089
+ executionIndicators,
1090
+ networkIndicators,
1091
+ processAliases,
1092
+ networkAliases,
1093
+ );
1094
+ });
1095
+ if (!path.node.specifiers?.length) {
1096
+ trackSuspiciousModuleReference(
1097
+ moduleName,
1098
+ undefined,
1099
+ executionIndicators,
1100
+ networkIndicators,
1101
+ processAliases,
1102
+ networkAliases,
1103
+ );
1104
+ }
1105
+ },
1106
+ VariableDeclarator: (path) => {
1107
+ const init = path?.node?.init;
1108
+ if (
1109
+ init?.type === "CallExpression" &&
1110
+ init.callee?.type === "Identifier" &&
1111
+ init.callee.name === "require"
1112
+ ) {
1113
+ const moduleName = getLiteralStringValue(init.arguments?.[0]);
1114
+ const localName =
1115
+ path?.node?.id?.type === "Identifier" ? path.node.id.name : undefined;
1116
+ trackSuspiciousModuleReference(
1117
+ moduleName,
1118
+ localName,
1119
+ executionIndicators,
1120
+ networkIndicators,
1121
+ processAliases,
1122
+ networkAliases,
1123
+ );
1124
+ }
1125
+ },
1126
+ CallExpression: (path) => {
1127
+ const callee = path?.node?.callee;
1128
+ const calleeChain = getMemberChainString(callee);
1129
+ if (callee?.type === "Identifier") {
1130
+ if (callee.name === "eval") {
1131
+ executionIndicators.add("eval");
1132
+ }
1133
+ if (callee.name === "atob") {
1134
+ obfuscationIndicators.add("atob");
1135
+ }
1136
+ if (["fetch", "axios", "got"].includes(callee.name)) {
1137
+ networkIndicators.add("network-request");
1138
+ }
1139
+ }
1140
+ if (calleeChain === "Buffer.from") {
1141
+ const encodingValue = getLiteralStringValue(path.node.arguments?.[1]);
1142
+ if (encodingValue?.toLowerCase() === "base64") {
1143
+ obfuscationIndicators.add("buffer-base64");
1144
+ }
1145
+ }
1146
+ if (calleeChain === "String.fromCharCode") {
1147
+ obfuscationIndicators.add("string-from-char-code");
1148
+ }
1149
+ if (calleeChain === "vm.runInNewContext") {
1150
+ executionIndicators.add("vm-run-context");
1151
+ obfuscationIndicators.add("vm-run-context");
1152
+ }
1153
+ if (calleeChain === "vm.runInThisContext") {
1154
+ executionIndicators.add("vm-run-context");
1155
+ obfuscationIndicators.add("vm-run-context");
1156
+ }
1157
+ if (callee?.type === "MemberExpression") {
1158
+ const objectName = getMemberChainString(callee.object);
1159
+ const propertyName = getMemberChainString(callee.property);
1160
+ if (
1161
+ objectName &&
1162
+ processAliases.has(objectName) &&
1163
+ SUSPICIOUS_JS_EXECUTION_MEMBERS.has(propertyName)
1164
+ ) {
1165
+ executionIndicators.add("child-process");
1166
+ }
1167
+ if (
1168
+ objectName &&
1169
+ networkAliases.has(objectName) &&
1170
+ SUSPICIOUS_JS_NETWORK_MEMBERS.has(propertyName)
1171
+ ) {
1172
+ networkIndicators.add("network-request");
1173
+ }
1174
+ }
1175
+ if (
1176
+ callee?.type === "Identifier" &&
1177
+ callee.name === "require" &&
1178
+ path.node.arguments?.length
1179
+ ) {
1180
+ const moduleName = getLiteralStringValue(path.node.arguments[0]);
1181
+ trackSuspiciousModuleReference(
1182
+ moduleName,
1183
+ undefined,
1184
+ executionIndicators,
1185
+ networkIndicators,
1186
+ processAliases,
1187
+ networkAliases,
1188
+ );
1189
+ }
1190
+ },
1191
+ NewExpression: (path) => {
1192
+ const calleeChain = getMemberChainString(path?.node?.callee);
1193
+ if (calleeChain === "Function") {
1194
+ executionIndicators.add("function-constructor");
1195
+ }
1196
+ },
1197
+ StringLiteral: (path) => {
1198
+ addSuspiciousLiteralIndicators(obfuscationIndicators, path?.node?.value);
1199
+ },
1200
+ TemplateElement: (path) => {
1201
+ addSuspiciousLiteralIndicators(
1202
+ obfuscationIndicators,
1203
+ path?.node?.value?.raw,
1204
+ );
1205
+ },
1206
+ });
1207
+ const indicators = [
1208
+ ...obfuscationIndicators,
1209
+ ...executionIndicators,
1210
+ ...networkIndicators,
1211
+ ].sort();
1212
+ return {
1213
+ executionIndicators: Array.from(executionIndicators).sort(),
1214
+ indicators,
1215
+ networkIndicators: Array.from(networkIndicators).sort(),
1216
+ obfuscationIndicators: Array.from(obfuscationIndicators).sort(),
1217
+ };
1218
+ }
1219
+
892
1220
  /**
893
1221
  * Find all imports and exports
894
1222
  */
895
1223
  export const findJSImportsExports = async (src, deep) => {
896
1224
  const allImports = {};
897
1225
  const allExports = {};
898
- const errFiles = [];
899
1226
  try {
900
1227
  const promiseMap = await getAllSrcJSAndTSFiles(src, deep);
901
1228
  const srcFiles = promiseMap.flat();
@@ -903,7 +1230,7 @@ export const findJSImportsExports = async (src, deep) => {
903
1230
  try {
904
1231
  parseFileASTTree(src, file, allImports, allExports);
905
1232
  } catch (_err) {
906
- errFiles.push(file);
1233
+ // ignore parse failures
907
1234
  }
908
1235
  }
909
1236
  return { allImports, allExports };
@@ -911,3 +1238,120 @@ export const findJSImportsExports = async (src, deep) => {
911
1238
  return { allImports, allExports };
912
1239
  }
913
1240
  };
1241
+
1242
+ /**
1243
+ * Detect suspicious obfuscation, execution, and network indicators in a single
1244
+ * JavaScript/TypeScript source file using Babel AST analysis.
1245
+ *
1246
+ * @param {string} filePath Source file path
1247
+ * @returns {{executionIndicators: string[], indicators: string[], networkIndicators: string[], obfuscationIndicators: string[]}}
1248
+ */
1249
+ export const analyzeSuspiciousJsFile = (filePath) => {
1250
+ let source;
1251
+ try {
1252
+ source = fileToParseableCode(filePath);
1253
+ } catch {
1254
+ return {
1255
+ executionIndicators: [],
1256
+ indicators: [],
1257
+ networkIndicators: [],
1258
+ obfuscationIndicators: [],
1259
+ };
1260
+ }
1261
+ return analyzeSuspiciousJsSource(source);
1262
+ };
1263
+
1264
+ /**
1265
+ * Detect browser-extension capability signals from source code using Babel AST analysis.
1266
+ *
1267
+ * @param {string} src Path to the extension source directory
1268
+ * @param {boolean} deep When true, includes node_modules and nested directories
1269
+ * @returns {{capabilities: string[], indicators: Object<string, string[]>}}
1270
+ * `indicators` is keyed by capability category name and contains arrays of
1271
+ * detected signal strings (for example property chains and call names).
1272
+ */
1273
+ export const detectExtensionCapabilities = (src, deep = false) => {
1274
+ const indicators = {};
1275
+ for (const category of CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES) {
1276
+ indicators[category] = new Set();
1277
+ }
1278
+ let srcFiles = [];
1279
+ try {
1280
+ srcFiles = [
1281
+ ...getAllFiles(deep, src, ".js"),
1282
+ ...getAllFiles(deep, src, ".jsx"),
1283
+ ...getAllFiles(deep, src, ".cjs"),
1284
+ ...getAllFiles(deep, src, ".mjs"),
1285
+ ...getAllFiles(deep, src, ".ts"),
1286
+ ...getAllFiles(deep, src, ".tsx"),
1287
+ ...getAllFiles(deep, src, ".vue"),
1288
+ ...getAllFiles(deep, src, ".svelte"),
1289
+ ];
1290
+ } catch (_err) {
1291
+ return { capabilities: [], indicators: {} };
1292
+ }
1293
+ const addSignalByPatterns = (rawSignal, patternMap) => {
1294
+ const signal = String(rawSignal || "").trim();
1295
+ if (!signal) {
1296
+ return;
1297
+ }
1298
+ for (const category of Object.keys(patternMap)) {
1299
+ const categoryPatterns = patternMap[category];
1300
+ const safePatterns = Array.isArray(categoryPatterns)
1301
+ ? categoryPatterns
1302
+ : [categoryPatterns];
1303
+ if (safePatterns.some((pattern) => pattern?.test(signal))) {
1304
+ indicators[category].add(signal);
1305
+ }
1306
+ }
1307
+ };
1308
+ const addSignal = (rawSignal) => {
1309
+ addSignalByPatterns(rawSignal, EXTENSION_CAPABILITY_CHAIN_PATTERNS);
1310
+ };
1311
+ const addIdentifierSignal = (rawSignal) => {
1312
+ addSignalByPatterns(rawSignal, EXTENSION_CAPABILITY_IDENTIFIER_PATTERNS);
1313
+ };
1314
+ for (const file of srcFiles) {
1315
+ try {
1316
+ const ast = parse(fileToParseableCode(file), babelParserOptions);
1317
+ traverse.default(ast, {
1318
+ MemberExpression: (path) => {
1319
+ addSignal(getMemberChainString(path?.node));
1320
+ },
1321
+ OptionalMemberExpression: (path) => {
1322
+ addSignal(getMemberChainString(path?.node));
1323
+ },
1324
+ CallExpression: (path) => {
1325
+ addSignal(getMemberChainString(path?.node?.callee));
1326
+ if (path?.node?.callee?.type === "Identifier") {
1327
+ addIdentifierSignal(path.node.callee.name);
1328
+ }
1329
+ },
1330
+ OptionalCallExpression: (path) => {
1331
+ addSignal(getMemberChainString(path?.node?.callee));
1332
+ if (path?.node?.callee?.type === "Identifier") {
1333
+ addIdentifierSignal(path.node.callee.name);
1334
+ }
1335
+ },
1336
+ NewExpression: (path) => {
1337
+ addSignal(getMemberChainString(path?.node?.callee));
1338
+ if (path?.node?.callee?.type === "Identifier") {
1339
+ addIdentifierSignal(path.node.callee.name);
1340
+ }
1341
+ },
1342
+ });
1343
+ } catch (_err) {
1344
+ // Skip parse failures and continue scanning
1345
+ }
1346
+ }
1347
+ const capabilityList = [];
1348
+ const indicatorMap = {};
1349
+ for (const category of CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES) {
1350
+ const sortedSignals = Array.from(indicators[category]).sort();
1351
+ if (sortedSignals.length) {
1352
+ capabilityList.push(category);
1353
+ indicatorMap[category] = sortedSignals;
1354
+ }
1355
+ }
1356
+ return { capabilities: capabilityList, indicators: indicatorMap };
1357
+ };
@@ -10,7 +10,11 @@ import { join } from "node:path";
10
10
 
11
11
  import { assert, describe, it } from "poku";
12
12
 
13
- import { findJSImportsExports } from "./analyzer.js";
13
+ import {
14
+ analyzeSuspiciousJsFile,
15
+ detectExtensionCapabilities,
16
+ findJSImportsExports,
17
+ } from "./analyzer.js";
14
18
 
15
19
  const baseTempDir = mkdtempSync(join(tmpdir(), "cdxgen-analyzer-poku-"));
16
20
 
@@ -228,3 +232,70 @@ wasi.start(instance);
228
232
  }
229
233
  });
230
234
  });
235
+
236
+ describe("detectExtensionCapabilities()", () => {
237
+ it("should detect extension capability signals from source usage", () => {
238
+ const projectDir = createProject(
239
+ "extension-capabilities",
240
+ `chrome.scripting.executeScript({ target: { tabId: 1 }, files: ["inject.js"] });
241
+ chrome.bluetooth.getDevices(() => {});
242
+ chrome.downloads.download({ url: "https://example.invalid/a.txt" });
243
+ const canvas = document.createElement("canvas");
244
+ canvas.toDataURL();
245
+ fetch("https://example.invalid/api");
246
+ navigator.userAgentData?.getHighEntropyValues(["platformVersion"]);
247
+ `,
248
+ );
249
+ const detected = detectExtensionCapabilities(projectDir);
250
+ assert.ok(detected.capabilities.includes("codeInjection"));
251
+ assert.ok(detected.capabilities.includes("bluetooth"));
252
+ assert.ok(detected.capabilities.includes("deviceAccess"));
253
+ assert.ok(detected.capabilities.includes("fileAccess"));
254
+ assert.ok(detected.capabilities.includes("network"));
255
+ assert.ok(detected.capabilities.includes("fingerprinting"));
256
+ });
257
+
258
+ it("should detect fingerprinting from canvas member-chain APIs", () => {
259
+ const projectDir = createProject(
260
+ "extension-capabilities-canvas-only",
261
+ `const canvas = document.createElement("canvas");
262
+ const ctx = canvas.getContext("2d");
263
+ ctx.getImageData(0, 0, 1, 1);
264
+ canvas.toDataURL();
265
+ ctx.measureText("a");
266
+ `,
267
+ );
268
+ const detected = detectExtensionCapabilities(projectDir);
269
+ assert.ok(detected.capabilities.includes("fingerprinting"));
270
+ });
271
+ });
272
+
273
+ describe("analyzeSuspiciousJsFile()", () => {
274
+ it("detects encoded child-process loader patterns", () => {
275
+ const projectDir = createProject(
276
+ "suspicious-lifecycle-js",
277
+ [
278
+ "import cp from 'node:child_process';",
279
+ "const payload = Buffer.from('ZXZhbCgnY29uc29sZS5sb2coMSknKQ==', 'base64');",
280
+ "cp.execSync(payload.toString());",
281
+ ].join("\n"),
282
+ );
283
+
284
+ const analysis = analyzeSuspiciousJsFile(join(projectDir, "index.js"));
285
+ assert.match(analysis.obfuscationIndicators.join(","), /buffer-base64/);
286
+ assert.match(analysis.executionIndicators.join(","), /child-process/);
287
+ });
288
+
289
+ it("detects network-capable script files referenced by lifecycle hooks", () => {
290
+ const projectDir = createProject(
291
+ "network-lifecycle-js",
292
+ [
293
+ "import https from 'node:https';",
294
+ "https.request('https://example.invalid/payload');",
295
+ ].join("\n"),
296
+ );
297
+
298
+ const analysis = analyzeSuspiciousJsFile(join(projectDir, "index.js"));
299
+ assert.match(analysis.networkIndicators.join(","), /network-request/);
300
+ });
301
+ });
@@ -0,0 +1,49 @@
1
+ function escapeMarkdownText(value) {
2
+ return String(value ?? "")
3
+ .replace(/&/g, "&amp;")
4
+ .replace(/</g, "&lt;")
5
+ .replace(/>/g, "&gt;")
6
+ .replace(/([\\`*_{}\[\]()#+!|])/g, "\\$1")
7
+ .replace(/\r?\n/g, "<br>")
8
+ .trim();
9
+ }
10
+
11
+ function escapeMarkdownCell(value) {
12
+ return escapeMarkdownText(value);
13
+ }
14
+
15
+ /**
16
+ * Format annotation properties as a markdown table for CycloneDX annotations.
17
+ *
18
+ * @param {{ name: string, value: string }[]} properties annotation properties
19
+ * @returns {string} markdown table text
20
+ */
21
+ export function propertiesToMarkdownTable(properties) {
22
+ if (!properties?.length) {
23
+ return "";
24
+ }
25
+ const lines = ["| Property | Value |", "| --- | --- |"];
26
+ for (const property of properties) {
27
+ lines.push(
28
+ `| ${escapeMarkdownCell(property?.name)} | ${escapeMarkdownCell(property?.value)} |`,
29
+ );
30
+ }
31
+ return lines.join("\n");
32
+ }
33
+
34
+ /**
35
+ * Build production-ready markdown annotation text.
36
+ *
37
+ * @param {string} message leading message text
38
+ * @param {{ name: string, value: string }[]} properties annotation properties
39
+ * @param {string[]} [details] optional detail lines shown before the table
40
+ * @returns {string} annotation text
41
+ */
42
+ export function buildAnnotationText(message, properties, details = []) {
43
+ const lines = [message, ...details.filter(Boolean)].map(escapeMarkdownText);
44
+ const markdownTable = propertiesToMarkdownTable(properties);
45
+ if (markdownTable) {
46
+ lines.push("", markdownTable);
47
+ }
48
+ return lines.join("\n");
49
+ }
@@ -0,0 +1,44 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ buildAnnotationText,
5
+ propertiesToMarkdownTable,
6
+ } from "./annotationFormatter.js";
7
+
8
+ describe("annotationFormatter", () => {
9
+ it("escapes markdown tables for untrusted annotation properties", () => {
10
+ const table = propertiesToMarkdownTable([
11
+ {
12
+ name: "<script>alert(1)</script>",
13
+ value: "[click](javascript:alert(1))\nnext|cell & more",
14
+ },
15
+ ]);
16
+
17
+ assert.strictEqual(
18
+ table,
19
+ [
20
+ "| Property | Value |",
21
+ "| --- | --- |",
22
+ String.raw`| &lt;script&gt;alert\(1\)&lt;/script&gt; | \[click\]\(javascript:alert\(1\)\)<br>next\|cell &amp; more |`,
23
+ ].join("\n"),
24
+ );
25
+ });
26
+
27
+ it("escapes markdown and HTML-sensitive content in annotation messages", () => {
28
+ const htmlLikeMessage =
29
+ String.fromCharCode(60) +
30
+ "img src=x onerror=alert(1)" +
31
+ String.fromCharCode(62);
32
+ const text = buildAnnotationText(
33
+ htmlLikeMessage,
34
+ [{ name: "cdx:test", value: "safe" }],
35
+ ["[open](javascript:alert(1))", "line\nbreak"],
36
+ );
37
+
38
+ assert.ok(text.startsWith("&lt;img src=x onerror=alert\\(1\\)&gt;"));
39
+ assert.ok(text.includes("\\[open\\]\\(javascript:alert\\(1\\)\\)"));
40
+ assert.ok(text.includes("line<br>break"));
41
+ assert.ok(text.includes("| cdx:test | safe |"));
42
+ assert.ok(!text.includes(htmlLikeMessage));
43
+ });
44
+ });
@@ -0,0 +1,36 @@
1
+ const SPDX_CONTEXT_PREFIX = "https://spdx.org/rdf/";
2
+ const CYCLONEDX_FORMAT = "CycloneDX";
3
+ const BOM_FORMAT_CYCLONEDX = "cyclonedx";
4
+ const BOM_FORMAT_SPDX = "spdx";
5
+ const BOM_FORMAT_UNKNOWN = "unknown";
6
+
7
+ export const isSpdxJsonLd = (bomJson) =>
8
+ Boolean(
9
+ bomJson?.["@context"]?.startsWith(SPDX_CONTEXT_PREFIX) &&
10
+ Array.isArray(bomJson?.["@graph"]) &&
11
+ bomJson["@graph"].some((element) => element?.type === "SpdxDocument"),
12
+ );
13
+
14
+ export const isCycloneDxBom = (bomJson) =>
15
+ bomJson?.bomFormat === CYCLONEDX_FORMAT && Boolean(bomJson?.specVersion);
16
+
17
+ export const detectBomFormat = (bomJson) => {
18
+ if (isCycloneDxBom(bomJson)) {
19
+ return BOM_FORMAT_CYCLONEDX;
20
+ }
21
+ if (isSpdxJsonLd(bomJson)) {
22
+ return BOM_FORMAT_SPDX;
23
+ }
24
+ return BOM_FORMAT_UNKNOWN;
25
+ };
26
+
27
+ export const getNonCycloneDxErrorMessage = (
28
+ bomJson,
29
+ commandName = "This command",
30
+ ) => {
31
+ const detectedFormat = detectBomFormat(bomJson);
32
+ if (detectedFormat === BOM_FORMAT_SPDX) {
33
+ return `${commandName} expects a CycloneDX BOM. SPDX input is not supported for this command.`;
34
+ }
35
+ return `${commandName} expects a CycloneDX JSON BOM.`;
36
+ };