@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.
- package/README.md +239 -90
- package/bin/audit.js +191 -0
- package/bin/cdxgen.js +513 -167
- package/bin/convert.js +99 -0
- package/bin/evinse.js +23 -0
- package/bin/repl.js +339 -8
- package/bin/sign.js +8 -0
- package/bin/validate.js +8 -0
- package/bin/verify.js +8 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/lolbas-index.json +150 -0
- package/data/queries-darwin.json +63 -3
- package/data/queries-win.json +45 -3
- package/data/queries.json +74 -2
- package/data/rules/chrome-extensions.yaml +240 -0
- package/data/rules/ci-permissions.yaml +478 -18
- package/data/rules/container-risk.yaml +270 -0
- package/data/rules/obom-runtime.yaml +891 -0
- package/data/rules/package-integrity.yaml +49 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-model-v3.0.1.jsonld +15999 -0
- package/lib/audit/index.js +1924 -0
- package/lib/audit/index.poku.js +1488 -0
- package/lib/audit/progress.js +137 -0
- package/lib/audit/progress.poku.js +188 -0
- package/lib/audit/reporters.js +618 -0
- package/lib/audit/scoring.js +310 -0
- package/lib/audit/scoring.poku.js +341 -0
- package/lib/audit/targets.js +260 -0
- package/lib/audit/targets.poku.js +331 -0
- package/lib/cli/index.js +154 -11
- package/lib/cli/index.poku.js +251 -0
- package/lib/helpers/analyzer.js +446 -2
- package/lib/helpers/analyzer.poku.js +72 -1
- package/lib/helpers/annotationFormatter.js +49 -0
- package/lib/helpers/annotationFormatter.poku.js +44 -0
- package/lib/helpers/bomUtils.js +36 -0
- package/lib/helpers/bomUtils.poku.js +51 -0
- package/lib/helpers/caxa.js +2 -2
- package/lib/helpers/chromextutils.js +1153 -0
- package/lib/helpers/chromextutils.poku.js +493 -0
- package/lib/helpers/ciParsers/githubActions.js +1632 -45
- package/lib/helpers/ciParsers/githubActions.poku.js +853 -1
- package/lib/helpers/containerRisk.js +186 -0
- package/lib/helpers/containerRisk.poku.js +52 -0
- package/lib/helpers/display.js +241 -59
- package/lib/helpers/display.poku.js +162 -2
- package/lib/helpers/exportUtils.js +123 -0
- package/lib/helpers/exportUtils.poku.js +60 -0
- package/lib/helpers/formulationParsers.js +69 -0
- package/lib/helpers/formulationParsers.poku.js +44 -0
- package/lib/helpers/gtfobins.js +189 -0
- package/lib/helpers/gtfobins.poku.js +49 -0
- package/lib/helpers/lolbas.js +267 -0
- package/lib/helpers/lolbas.poku.js +39 -0
- package/lib/helpers/osqueryTransform.js +84 -0
- package/lib/helpers/osqueryTransform.poku.js +49 -0
- package/lib/helpers/provenanceUtils.js +193 -0
- package/lib/helpers/provenanceUtils.poku.js +145 -0
- package/lib/helpers/pylockutils.js +281 -0
- package/lib/helpers/pylockutils.poku.js +48 -0
- package/lib/helpers/registryProvenance.js +793 -0
- package/lib/helpers/registryProvenance.poku.js +452 -0
- package/lib/helpers/source.js +1267 -0
- package/lib/helpers/source.poku.js +771 -0
- package/lib/helpers/spdxUtils.js +97 -0
- package/lib/helpers/spdxUtils.poku.js +70 -0
- package/lib/helpers/unicodeScan.js +147 -0
- package/lib/helpers/unicodeScan.poku.js +45 -0
- package/lib/helpers/utils.js +700 -128
- package/lib/helpers/utils.poku.js +877 -80
- package/lib/managers/binary.js +29 -5
- package/lib/managers/docker.js +179 -52
- package/lib/managers/docker.poku.js +327 -28
- package/lib/managers/oci.js +107 -23
- package/lib/managers/oci.poku.js +132 -0
- package/lib/server/openapi.yaml +17 -0
- package/lib/server/server.js +225 -336
- package/lib/server/server.poku.js +16 -10
- package/lib/stages/postgen/annotator.js +7 -0
- package/lib/stages/postgen/annotator.poku.js +40 -0
- package/lib/stages/postgen/auditBom.js +19 -3
- package/lib/stages/postgen/auditBom.poku.js +1729 -67
- package/lib/stages/postgen/postgen.js +40 -0
- package/lib/stages/postgen/postgen.poku.js +47 -0
- package/lib/stages/postgen/ruleEngine.js +80 -2
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/spdxConverter.poku.js +341 -0
- package/lib/validator/bomValidator.js +232 -0
- package/lib/validator/bomValidator.poku.js +70 -0
- package/lib/validator/complianceRules.js +70 -7
- package/lib/validator/complianceRules.poku.js +30 -0
- package/lib/validator/reporters/annotations.js +2 -2
- package/lib/validator/reporters/console.js +11 -0
- package/lib/validator/reporters.poku.js +13 -0
- package/package.json +10 -7
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +115 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +27 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +35 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +35 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +63 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +8 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +13 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomUtils.d.ts +5 -0
- package/types/lib/helpers/bomUtils.d.ts.map +1 -0
- package/types/lib/helpers/chromextutils.d.ts +97 -0
- package/types/lib/helpers/chromextutils.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +3 -8
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/containerRisk.d.ts +17 -0
- package/types/lib/helpers/containerRisk.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +4 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/exportUtils.d.ts +40 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +17 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts +16 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -0
- package/types/lib/helpers/osqueryTransform.d.ts +7 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -0
- package/types/lib/helpers/provenanceUtils.d.ts +90 -0
- package/types/lib/helpers/provenanceUtils.d.ts.map +1 -0
- package/types/lib/helpers/pylockutils.d.ts +51 -0
- package/types/lib/helpers/pylockutils.d.ts.map +1 -0
- package/types/lib/helpers/registryProvenance.d.ts +17 -0
- package/types/lib/helpers/registryProvenance.d.ts.map +1 -0
- package/types/lib/helpers/source.d.ts +141 -0
- package/types/lib/helpers/source.d.ts.map +1 -0
- package/types/lib/helpers/spdxUtils.d.ts +2 -0
- package/types/lib/helpers/spdxUtils.d.ts.map +1 -0
- package/types/lib/helpers/unicodeScan.d.ts +46 -0
- package/types/lib/helpers/unicodeScan.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +29 -11
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +0 -36
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +1 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/bin/dependencies.d.ts +0 -3
- package/types/bin/dependencies.d.ts.map +0 -1
- package/types/bin/licenses.d.ts +0 -3
- package/types/bin/licenses.d.ts.map +0 -1
package/lib/helpers/analyzer.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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, "&")
|
|
4
|
+
.replace(/</g, "<")
|
|
5
|
+
.replace(/>/g, ">")
|
|
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`| <script>alert\(1\)</script> | \[click\]\(javascript:alert\(1\)\)<br>next\|cell & 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("<img src=x onerror=alert\\(1\\)>"));
|
|
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
|
+
};
|