@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
1,880 lines (1,819 loc) • 157 kB
JavaScript
import { lstatSync, readdirSync, readFileSync } from "node:fs";
import {
basename,
isAbsolute,
join,
matchesGlob,
relative,
resolve,
} from "node:path";
import process from "node:process";
import { URL } from "node:url";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import {
getScopedStaticValueByName,
getStaticObjectProperty,
resolveStaticValue,
} from "./analyzerScope.js";
import { classifyMcpReference } from "./mcp.js";
import { isLocalHost, sanitizeMcpRefToken } from "./mcpDiscovery.js";
import {
sanitizeBomPropertyValue,
sanitizeBomUrl,
} from "./propertySanitizer.js";
const IGNORE_DIRS = process.env.ASTGEN_IGNORE_DIRS
? process.env.ASTGEN_IGNORE_DIRS.split(",")
: [
"venv",
"docs",
"test",
"tests",
"e2e",
"examples",
"cypress",
"site-packages",
"typings",
"api_docs",
"dev_docs",
"types",
"mock",
"mocks",
"jest-cache",
"eslint-rules",
"codemods",
"flow-typed",
"i18n",
"coverage",
];
const IGNORE_FILE_PATTERN = new RegExp(
process.env.ASTGEN_IGNORE_FILE_PATTERN ||
"(conf|config|test|spec|mock|setup-jest|\\.d)\\.(js|ts|tsx)$",
"i",
);
const normalizeAnalyzerPathForGlob = (filePath) =>
String(filePath || "").replaceAll("\\", "/");
const normalizeAnalyzerSearchOptions = (deepOrOptions = false) => {
if (deepOrOptions && typeof deepOrOptions === "object") {
return {
deep: Boolean(deepOrOptions.deep),
exclude: Array.isArray(deepOrOptions.exclude)
? deepOrOptions.exclude
: [],
};
}
return {
deep: Boolean(deepOrOptions),
exclude: [],
};
};
const shouldExcludeAnalyzerPath = (
rootDir,
filePath,
excludePatterns,
isDirectory = false,
) => {
if (!excludePatterns?.length) {
return false;
}
const normalizedAbsolutePath = normalizeAnalyzerPathForGlob(
resolve(filePath),
);
const normalizedRelativePath = normalizeAnalyzerPathForGlob(
relative(rootDir, filePath),
);
const candidatePaths = [
normalizedAbsolutePath,
normalizedRelativePath,
normalizedRelativePath ? `./${normalizedRelativePath}` : "",
].filter(Boolean);
if (isDirectory) {
candidatePaths.push(
`${normalizedAbsolutePath}/`,
normalizedRelativePath ? `${normalizedRelativePath}/` : "",
normalizedRelativePath ? `./${normalizedRelativePath}/` : "",
);
}
return excludePatterns.some((pattern) => {
const normalizedPattern = normalizeAnalyzerPathForGlob(pattern);
return candidatePaths.some((candidatePath) =>
matchesGlob(candidatePath, normalizedPattern),
);
});
};
const getAllFiles = (
deep,
dir,
extn,
files,
result,
regex,
rootDir,
excludePatterns,
) => {
files = files || readdirSync(dir);
result = result || [];
regex = regex || new RegExp(`\\${extn}$`);
rootDir = rootDir || dir;
excludePatterns = excludePatterns || [];
for (let i = 0; i < files.length; i++) {
if (IGNORE_FILE_PATTERN.test(files[i]) || files[i].startsWith(".")) {
continue;
}
const file = join(dir, files[i]);
const fileStat = lstatSync(file);
if (fileStat.isSymbolicLink()) {
continue;
}
if (
shouldExcludeAnalyzerPath(
rootDir,
file,
excludePatterns,
fileStat.isDirectory(),
)
) {
continue;
}
if (fileStat.isDirectory()) {
// Ignore directories
const dirName = basename(file);
if (
dirName.startsWith(".") ||
dirName.startsWith("__") ||
IGNORE_DIRS.includes(dirName.toLowerCase())
) {
continue;
}
// We need to include node_modules in deep mode to track exports
// Ignore only for non-deep analysis
if (!deep && dirName === "node_modules") {
continue;
}
try {
result = getAllFiles(
deep,
file,
extn,
readdirSync(file),
result,
regex,
rootDir,
excludePatterns,
);
} catch (_error) {
// ignore
}
} else {
if (regex.test(file)) {
result.push(file);
}
}
}
return result;
};
const babelParserOptions = {
sourceType: "unambiguous",
allowImportExportEverywhere: true,
allowAwaitOutsideFunction: true,
allowNewTargetOutsideFunction: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
errorRecovery: true,
allowUndeclaredExports: true,
createImportExpressions: true,
tokens: true,
attachComment: false,
plugins: [
"optionalChaining",
"classProperties",
"decorators-legacy",
"exportDefaultFrom",
"doExpressions",
"numericSeparator",
"dynamicImport",
"jsx",
"typescript",
],
};
/**
* Filter only references to (t|jsx?) or (less|scss) files for now.
* Opt to use our relative paths.
*/
const setFileRef = (
allImports,
allExports,
src,
file,
pathnode,
specifiers = [],
) => {
const pathway = pathnode.value || pathnode.name;
const sourceLoc = pathnode.loc?.start;
if (!pathway) {
return;
}
const fileRelativeLoc = relative(src, file);
// remove unexpected extension imports
if (/\.(svg|png|jpg|json|d\.ts)/.test(pathway)) {
return;
}
const importedModules = specifiers
.map((s) => s.imported?.name)
.filter((v) => v !== undefined);
const exportedModules = specifiers
.map((s) => s.exported?.name)
.filter((v) => v !== undefined);
const occurrence = {
importedAs: pathway,
importedModules,
exportedModules,
isExternal: true,
fileName: fileRelativeLoc,
lineNumber: sourceLoc?.line ?? undefined,
columnNumber: sourceLoc?.column ?? undefined,
};
// replace relative imports with full path
let moduleFullPath = pathway;
let wasAbsolute = false;
if (/\.\//g.test(pathway) || /\.\.\//g.test(pathway)) {
moduleFullPath = resolve(file, "..", pathway);
if (isAbsolute(moduleFullPath)) {
moduleFullPath = relative(src, moduleFullPath);
wasAbsolute = true;
}
if (!moduleFullPath.startsWith("node_modules/")) {
occurrence.isExternal = false;
}
}
allImports[moduleFullPath] = allImports[moduleFullPath] || new Set();
allImports[moduleFullPath].add(occurrence);
// Handle module package name
// Eg: zone.js/dist/zone will be referred to as zone.js in package.json
if (!wasAbsolute && moduleFullPath.includes("/")) {
const modPkg = moduleFullPath.split("/")[0];
allImports[modPkg] = allImports[modPkg] || new Set();
allImports[modPkg].add(occurrence);
}
if (exportedModules?.length) {
moduleFullPath = moduleFullPath
.replace("node_modules/", "")
.replace("dist/", "")
.replace(/\.(js|ts|cjs|mjs)$/g, "")
.replace("src/", "");
allExports[moduleFullPath] = allExports[moduleFullPath] || new Set();
occurrence.exportedModules = exportedModules;
allExports[moduleFullPath].add(occurrence);
}
};
const vueCleaningRegex = /<\/*script.*>|<style[\s\S]*style>|<\/*br>/gi;
const vueTemplateRegex = /(<template.*>)([\s\S]*)(<\/template>)/gi;
const vueCommentRegex = /<!--[\s\S]*?-->/gi;
const vueBindRegex = /(:\[)([\s\S]*?)(])/gi;
const vuePropRegex = /\s([.:@])([a-zA-Z]*?=)/gi;
const fileToParseableCode = (file) => {
let code = readFileSync(file, "utf-8");
if (file.endsWith(".vue") || file.endsWith(".svelte")) {
code = code
.replace(vueCommentRegex, (match) => match.replaceAll(/\S/g, " "))
.replace(
vueCleaningRegex,
(match) => `${match.replaceAll(/\S/g, " ").substring(1)};`,
)
.replace(
vueBindRegex,
(_match, grA, grB, grC) =>
grA.replaceAll(/\S/g, " ") + grB + grC.replaceAll(/\S/g, " "),
)
.replace(
vuePropRegex,
(_match, grA, grB) => ` ${grA.replace(/[.:@]/g, " ")}${grB}`,
)
.replace(
vueTemplateRegex,
(_match, grA, grB, grC) =>
grA + grB.replaceAll("{{", "{ ").replaceAll("}}", " }") + grC,
);
}
return code;
};
const isWasmPath = (modulePath) =>
typeof modulePath === "string" && /\.wasm([?#].*)?$/i.test(modulePath);
const getStringValue = (astNode) => {
if (!astNode) {
return undefined;
}
if (astNode.type === "StringLiteral") {
return astNode.value;
}
if (
astNode.type === "TemplateLiteral" &&
astNode.expressions.length === 0 &&
astNode.quasis.length === 1
) {
return astNode.quasis[0].value.cooked;
}
return undefined;
};
const ANGULAR_WORKSPACE_MANIFEST_NAMES = new Set([
"angular.json",
"workspace.json",
"project.json",
]);
const ANGULAR_RESOURCE_METADATA_KEYS = new Set([
"styleUrl",
"styleUrls",
"templateUrl",
]);
const ANGULAR_PACKAGE_STRING_KEYS = new Set([
"builder",
"executor",
"loadChildren",
]);
const ANGULAR_WORKSPACE_PACKAGE_STRING_KEYS = new Set([
"builder",
"executor",
"polyfills",
"plugins",
"scripts",
"styles",
]);
const ANGULAR_CONFIG_EVIDENCE_FILE_NAMES = new Set([
"karma.conf.js",
"karma.conf.cjs",
"karma.conf.mjs",
"protractor.conf.js",
"protractor.conf.cjs",
"protractor.conf.mjs",
]);
const ANGULAR_SCRIPT_COMMAND_PACKAGES = [
{ pattern: /(?:^|[\s;&|()])ng\s+test(?:\s|$)/, packages: ["karma"] },
{ pattern: /(?:^|[\s;&|()])ng\s+e2e(?:\s|$)/, packages: ["protractor"] },
{
pattern: /(?:^|[\s;&|()])ng\s+lint(?:\s|$)/,
packages: ["codelyzer", "tslint"],
},
{
pattern: /(?:^|[\s;&|()])ng\s+(?:build|serve|run|xi18n)(?:\s|$)/,
packages: ["@angular/compiler-cli", "typescript"],
},
{ pattern: /(?:^|[\s;&|()])ng(?:\s|$)/, packages: ["@angular/cli"] },
{
pattern: /(?:^|[\s;&|()])ngc(?:\s|$)/,
packages: ["@angular/compiler-cli"],
},
{ pattern: /(?:^|[\s;&|()])tsc(?:\s|$)/, packages: ["typescript"] },
{ pattern: /(?:^|[\s;&|()])ttsc(?:\s|$)/, packages: ["ttypescript"] },
{ pattern: /(?:^|[\s;&|()])ts-node(?:\s|\/|$)/, packages: ["ts-node"] },
{ pattern: /(?:^|[\s;&|()])tslint(?:\s|$)/, packages: ["tslint"] },
{ pattern: /(?:^|[\s;&|()])karma(?:\s|$)/, packages: ["karma"] },
{ pattern: /(?:^|[\s;&|()])protractor(?:\s|$)/, packages: ["protractor"] },
{ pattern: /(?:^|[\s;&|()])jasmine(?:\s|$)/, packages: ["jasmine"] },
{
pattern: /(?:^|[\s;&|()])webpack-bundle-analyzer(?:\s|$)/,
packages: ["webpack-bundle-analyzer"],
},
{ pattern: /(?:^|[\s;&|()])ng-packagr(?:\s|$)/, packages: ["ng-packagr"] },
{
pattern: /(?:^|[\s;&|()])firebase-tools(?:\s|$)/,
packages: ["firebase-tools"],
},
{ pattern: /(?:^|[\s;&|()])typedoc(?:\s|$)/, packages: ["typedoc"] },
{ pattern: /(?:^|[\s;&|()])rimraf(?:\s|$)/, packages: ["rimraf"] },
{ pattern: /(?:^|[\s;&|()])rollup(?:\s|$)/, packages: ["rollup"] },
];
const ANGULAR_KARMA_CONFIG_PATTERNS = [
{
pattern: /\bframeworks\s*:\s*\[[^\]]*['"]jasmine['"]/s,
packages: ["karma-jasmine", "jasmine-core"],
},
{
pattern: /\bbrowsers\s*:\s*\[[^\]]*['"]Chrome/s,
packages: ["karma-chrome-launcher"],
},
{
pattern: /\bbrowsers\s*:\s*\[[^\]]*['"]Firefox/s,
packages: ["karma-firefox-launcher"],
},
{
pattern: /\bbrowsers\s*:\s*\[[^\]]*['"]Safari/s,
packages: ["karma-safarinative-launcher"],
},
{
pattern: /\breporters\s*:\s*\[[^\]]*['"]coverage-istanbul['"]/s,
packages: ["karma-coverage-istanbul-reporter"],
},
{
pattern: /\breporters\s*:\s*\[[^\]]*['"]html['"]/s,
packages: ["karma-jasmine-html-reporter"],
},
];
const ANGULAR_PROTRACTOR_CONFIG_PATTERNS = [
{
pattern: /\bframework\s*:\s*['"]jasmine['"]/,
packages: ["jasmine-core", "jasminewd2"],
},
{ pattern: /\bts-node\/register\b/, packages: ["ts-node"] },
];
const isAngularTsconfigFileName = (fileName) =>
/^tsconfig(?:[.-].*)?\.json$/i.test(fileName);
const ANGULAR_TEMPLATE_PACKAGE_PATTERNS = [
{
packageName: "@angular/router",
pattern: /<\s*router-outlet\b|\brouterLink(?:Active)?\b/,
},
{
packageName: "@angular/common",
pattern:
/\*(ngIf|ngFor|ngSwitch)|\b(ngClass|ngStyle|ngTemplateOutlet)\b|\|\s*(async|date|currency|decimal|json|keyvalue|lowercase|number|percent|slice|titlecase|uppercase)\b/,
},
{
packageName: "@angular/forms",
pattern:
/\b(formControl|formControlName|formGroup|formArrayName|ngModel)\b/,
},
{
packageName: "@angular/material",
pattern: /<\s*mat-[\w-]+\b|\bmat[A-Z][\w-]*\b|\bmat-[\w-]+\b/,
},
{
packageName: "@angular/cdk",
pattern: /<\s*cdk-[\w-]+\b|\bcdk[A-Z][\w-]*\b|\bcdk-[\w-]+\b/,
},
{
packageName: "@ionic/angular",
pattern: /<\s*ion-[\w-]+\b/,
},
{
packageName: "@fortawesome/angular-fontawesome",
pattern: /<\s*fa-icon\b/,
},
{
packageName: "ag-grid-angular",
pattern: /<\s*ag-grid-angular\b/,
},
{
packageName: "@ng-select/ng-select",
pattern: /<\s*ng-select\b/,
},
{
packageName: "@swimlane/ngx-charts",
pattern: /<\s*ngx-charts-[\w-]+\b/,
},
];
const angularLocalPathPrefixRegex =
/^(\.|\/|\\|src\/|app\/|assets\/|styles\/|environments\/)/i;
const angularUrlRegex = /^[a-z][a-z0-9+.-]*:/i;
const angularBareFileRegex =
/^[\w.-]+\.(css|html|js|json|less|mjs|scss|sass|ts)$/i;
const stripAngularRouteFragment = (reference) =>
String(reference || "")
.split("#")[0]
.split("?")[0]
.trim();
const extractAngularPackageName = (reference, allowBare = true) => {
let candidate = stripAngularRouteFragment(reference)
.replace(/^~+/, "")
.replace(/^\.\//, "");
if (!candidate || angularUrlRegex.test(candidate)) {
return undefined;
}
candidate = candidate.replaceAll("\\", "/");
if (candidate.startsWith("node_modules/")) {
candidate = candidate.slice("node_modules/".length);
} else if (angularLocalPathPrefixRegex.test(candidate)) {
return undefined;
}
if (!allowBare && !candidate.includes("/")) {
return undefined;
}
if (candidate === "zone.js") {
return candidate;
}
if (angularBareFileRegex.test(candidate)) {
return undefined;
}
if (candidate.startsWith("@")) {
const scopedMatch = candidate.match(/^(@[^/\s:]+\/[^/\s:]+)/);
return scopedMatch?.[1];
}
const unscopedMatch = candidate.match(
/^([a-zA-Z0-9][a-zA-Z0-9._-]*)(?=\/|:|$)/,
);
return unscopedMatch?.[1];
};
const setAngularPackageRef = (
allImports,
allExports,
src,
file,
reference,
sourceLoc,
allowBare = true,
) => {
const packageName = extractAngularPackageName(reference, allowBare);
if (!packageName) {
return;
}
setSyntheticImportRef(
allImports,
allExports,
src,
file,
packageName,
[],
sourceLoc,
);
};
const getObjectPropertyKeyName = (propertyNode) => {
if (!propertyNode) {
return undefined;
}
return getMemberExpressionPropertyName(propertyNode);
};
const getAngularStringNodes = (astNode) => {
if (!astNode) {
return [];
}
if (getStringValue(astNode)) {
return [astNode];
}
if (astNode.type === "ArrayExpression") {
return (astNode.elements || []).filter((element) =>
getStringValue(element),
);
}
return [];
};
const unwrapAwait = (astNode) =>
astNode?.type === "AwaitExpression" ? astNode.argument : astNode;
const isImportMetaUrl = (astNode) =>
astNode?.type === "MemberExpression" &&
astNode.object?.type === "MetaProperty" &&
astNode.object.meta?.name === "import" &&
astNode.object.property?.name === "meta" &&
astNode.property?.type === "Identifier" &&
astNode.property.name === "url";
const getMemberExpressionPropertyName = (propertyNode) => {
if (!propertyNode) {
return undefined;
}
if (propertyNode.type === "Identifier") {
return propertyNode.name;
}
if (propertyNode.type === "StringLiteral") {
return propertyNode.value;
}
return undefined;
};
const resolveWasmLiteralFromNode = (astNode, wasmBufferByVarName) => {
const normalizedNode = unwrapAwait(astNode);
const directLiteral = getStringValue(normalizedNode);
if (isWasmPath(directLiteral)) {
return directLiteral;
}
if (normalizedNode?.type === "Identifier") {
return wasmBufferByVarName.get(normalizedNode.name);
}
if (normalizedNode?.type === "CallExpression") {
if (
normalizedNode.callee?.type === "Identifier" &&
normalizedNode.callee.name === "fetch" &&
normalizedNode.arguments?.length
) {
return resolveWasmLiteralFromNode(
normalizedNode.arguments[0],
wasmBufferByVarName,
);
}
}
if (normalizedNode?.type === "NewExpression") {
if (
normalizedNode.callee?.type === "Identifier" &&
normalizedNode.callee.name === "URL" &&
normalizedNode.arguments?.length
) {
const urlLiteral = getStringValue(normalizedNode.arguments[0]);
const baseArg = normalizedNode.arguments[1];
if (isWasmPath(urlLiteral) && (!baseArg || isImportMetaUrl(baseArg))) {
return urlLiteral;
}
}
}
return undefined;
};
const getWasmSourceFromInstantiateCall = (callNode, wasmBufferByVarName) => {
if (!callNode?.callee || callNode.callee.type !== "MemberExpression") {
return undefined;
}
const objectNode = callNode.callee.object;
const propertyNode = callNode.callee.property;
const calleeObjectName = getMemberExpressionPropertyName(objectNode);
const calleePropertyName = getMemberExpressionPropertyName(propertyNode);
if (calleeObjectName !== "WebAssembly") {
return undefined;
}
if (
calleePropertyName !== "instantiate" &&
calleePropertyName !== "instantiateStreaming" &&
calleePropertyName !== "compile" &&
calleePropertyName !== "compileStreaming"
) {
return undefined;
}
if (!callNode.arguments?.length) {
return undefined;
}
return resolveWasmLiteralFromNode(callNode.arguments[0], wasmBufferByVarName);
};
const getWasmSourceFromCallExpression = (callNode, wasmBufferByVarName) => {
const wasmSourceFromInstantiate = getWasmSourceFromInstantiateCall(
callNode,
wasmBufferByVarName,
);
if (wasmSourceFromInstantiate) {
return wasmSourceFromInstantiate;
}
if (
callNode?.callee?.type === "Identifier" &&
["fetch", "locateFile"].includes(callNode.callee.name) &&
callNode.arguments?.length
) {
return resolveWasmLiteralFromNode(
callNode.arguments[0],
wasmBufferByVarName,
);
}
return undefined;
};
const getNamedImportsFromObjectPattern = (idNode) => {
const namedImports = [];
if (!idNode || idNode.type !== "ObjectPattern") {
return namedImports;
}
for (const prop of idNode.properties || []) {
if (prop.type !== "ObjectProperty") {
continue;
}
const keyName = getMemberExpressionPropertyName(prop.key);
if (keyName) {
namedImports.push(keyName);
}
}
return namedImports;
};
const setSyntheticImportRef = (
allImports,
allExports,
src,
file,
importPath,
modules,
sourceLoc,
) => {
if (!importPath) {
return;
}
const safeModules = modules || [];
const syntheticSpecifiers = safeModules.map((moduleName) => ({
imported: { name: moduleName },
}));
setFileRef(
allImports,
allExports,
src,
file,
{ value: importPath, loc: sourceLoc ? { start: sourceLoc } : undefined },
syntheticSpecifiers,
);
};
const setSyntheticExportRef = (
allImports,
allExports,
src,
file,
importPath,
modules,
sourceLoc,
) => {
if (!importPath) {
return;
}
const safeModules = modules || [];
const syntheticSpecifiers = safeModules.map((moduleName) => ({
exported: { name: moduleName },
}));
setFileRef(
allImports,
allExports,
src,
file,
{ value: importPath, loc: sourceLoc ? { start: sourceLoc } : undefined },
syntheticSpecifiers,
);
};
const getWasmExportMemberInfo = (astNode) => {
if (!astNode) {
return undefined;
}
if (astNode.type === "AssignmentExpression") {
return getWasmExportMemberInfo(astNode.right);
}
if (
astNode.type !== "MemberExpression" ||
astNode.object?.type !== "Identifier"
) {
return undefined;
}
return {
aliasName: astNode.object.name,
exportName: getMemberExpressionPropertyName(astNode.property),
};
};
const getAssignmentTargetName = (astNode) => {
if (!astNode) {
return undefined;
}
if (astNode.type === "Identifier") {
return astNode.name;
}
if (
astNode.type === "MemberExpression" &&
astNode.object?.type === "Identifier" &&
astNode.object.name === "Module"
) {
return getMemberExpressionPropertyName(astNode.property);
}
return undefined;
};
/**
* Check AST tree for any (j|tsx?) files and set a file
* references for any import, require or dynamic import files.
*/
const parseFileASTTree = (src, file, allImports, allExports) => {
const ast = parse(fileToParseableCode(file), babelParserOptions);
const wasmBufferByVarName = new Map();
const wasmResultByVarName = new Map();
const wasmInstanceByVarName = new Map();
const wasiConstructorAliases = new Set(["WASI"]);
const wasiNamespaceAliases = new Set();
const wasiInstanceAliases = new Set();
const wasmPathLiterals = new Set();
const wasmExportAliases = new Set(["wasmExports"]);
traverse.default(ast, {
ImportDeclaration: (path) => {
if (path?.node) {
setFileRef(
allImports,
allExports,
src,
file,
path.node.source,
path.node.specifiers,
);
const sourceValue = path.node.source?.value;
if (sourceValue === "@angular/platform-browser/animations") {
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"@angular/animations",
[],
path.node.loc?.start,
);
}
if (sourceValue === "@angular/platform-browser-dynamic") {
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"@angular/compiler",
[],
path.node.loc?.start,
);
}
if (
sourceValue === "@angular/fire" ||
sourceValue?.startsWith("@angular/fire/")
) {
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"firebase",
[],
path.node.loc?.start,
);
}
if (sourceValue === "node:wasi" || sourceValue === "wasi") {
for (const specifier of path.node.specifiers || []) {
if (
specifier.type === "ImportSpecifier" &&
specifier.imported?.name === "WASI"
) {
wasiConstructorAliases.add(specifier.local?.name || "WASI");
}
if (specifier.type === "ImportNamespaceSpecifier") {
wasiNamespaceAliases.add(specifier.local?.name);
}
}
}
}
},
// For require('') statements
Identifier: (path) => {
if (
path?.node &&
path.node.name === "require" &&
path.parent.type === "CallExpression"
) {
setFileRef(allImports, allExports, src, file, path.parent.arguments[0]);
}
},
// Use for dynamic imports like routes.jsx
CallExpression: (path) => {
if (path?.node && path.node.callee.type === "Import") {
setFileRef(allImports, allExports, src, file, path.node.arguments[0]);
}
const wasmSourceLiteral = getWasmSourceFromCallExpression(
path?.node,
wasmBufferByVarName,
);
if (wasmSourceLiteral) {
wasmPathLiterals.add(wasmSourceLiteral);
setSyntheticImportRef(
allImports,
allExports,
src,
file,
wasmSourceLiteral,
[],
path.node.loc?.start,
);
}
if (
path?.node?.callee?.type === "MemberExpression" &&
path.node.callee.object?.type === "Identifier" &&
wasiInstanceAliases.has(path.node.callee.object.name)
) {
const methodName = getMemberExpressionPropertyName(
path.node.callee.property,
);
if (methodName === "start" || methodName === "initialize") {
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"node:wasi",
[methodName],
path.node.loc?.start,
);
}
}
},
ImportExpression: (path) => {
if (path?.node?.source) {
setFileRef(allImports, allExports, src, file, path.node.source);
}
},
VariableDeclarator: (path) => {
const idNode = path?.node?.id;
const initNode = unwrapAwait(path?.node?.init);
if (!idNode || !initNode) {
return;
}
if (
idNode.type === "Identifier" &&
initNode.type === "CallExpression" &&
initNode.callee?.type === "MemberExpression"
) {
const calleePropertyName = getMemberExpressionPropertyName(
initNode.callee.property,
);
if (
calleePropertyName === "readFile" ||
calleePropertyName === "readFileSync"
) {
const pathArg = initNode.arguments?.[0];
const wasmPath = getStringValue(pathArg);
if (isWasmPath(wasmPath)) {
wasmBufferByVarName.set(idNode.name, wasmPath);
wasmPathLiterals.add(wasmPath);
setSyntheticImportRef(
allImports,
allExports,
src,
file,
wasmPath,
[],
path.node.loc?.start,
);
}
}
const wasmSource = getWasmSourceFromInstantiateCall(
initNode,
wasmBufferByVarName,
);
if (wasmSource) {
wasmResultByVarName.set(idNode.name, wasmSource);
wasmPathLiterals.add(wasmSource);
setSyntheticImportRef(
allImports,
allExports,
src,
file,
wasmSource,
[],
path.node.loc?.start,
);
}
if (
initNode.callee?.type === "MemberExpression" &&
initNode.callee.object?.type === "Identifier" &&
wasiNamespaceAliases.has(initNode.callee.object.name) &&
getMemberExpressionPropertyName(initNode.callee.property) === "WASI"
) {
wasiInstanceAliases.add(idNode.name);
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"node:wasi",
["WASI"],
path.node.loc?.start,
);
}
}
if (
idNode.type === "Identifier" &&
initNode.type === "CallExpression" &&
initNode.callee?.type === "Identifier" &&
wasiConstructorAliases.has(initNode.callee.name)
) {
wasiInstanceAliases.add(idNode.name);
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"node:wasi",
["WASI"],
path.node.loc?.start,
);
}
if (idNode.type === "Identifier" && initNode.type === "NewExpression") {
if (
initNode.callee?.type === "Identifier" &&
wasiConstructorAliases.has(initNode.callee.name)
) {
wasiInstanceAliases.add(idNode.name);
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"node:wasi",
["WASI"],
path.node.loc?.start,
);
}
if (
initNode.callee?.type === "MemberExpression" &&
initNode.callee.object?.type === "Identifier" &&
wasiNamespaceAliases.has(initNode.callee.object.name) &&
getMemberExpressionPropertyName(initNode.callee.property) === "WASI"
) {
wasiInstanceAliases.add(idNode.name);
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"node:wasi",
["WASI"],
path.node.loc?.start,
);
}
}
if (idNode.type === "ObjectPattern") {
if (initNode.type === "CallExpression") {
const wasmSource = getWasmSourceFromInstantiateCall(
initNode,
wasmBufferByVarName,
);
if (wasmSource) {
wasmPathLiterals.add(wasmSource);
for (const prop of idNode.properties || []) {
if (
prop.type === "ObjectProperty" &&
getMemberExpressionPropertyName(prop.key) === "instance" &&
prop.value?.type === "Identifier"
) {
wasmInstanceByVarName.set(prop.value.name, wasmSource);
}
}
setSyntheticImportRef(
allImports,
allExports,
src,
file,
wasmSource,
[],
path.node.loc?.start,
);
}
if (
initNode.callee?.type === "Identifier" &&
initNode.callee.name === "require"
) {
const requiredModule = getStringValue(initNode.arguments?.[0]);
if (requiredModule === "node:wasi" || requiredModule === "wasi") {
for (const prop of idNode.properties || []) {
if (
prop.type === "ObjectProperty" &&
getMemberExpressionPropertyName(prop.key) === "WASI" &&
prop.value?.type === "Identifier"
) {
wasiConstructorAliases.add(prop.value.name);
}
}
}
}
}
if (initNode.type === "MemberExpression") {
const exportNames = getNamedImportsFromObjectPattern(idNode);
if (!exportNames.length) {
return;
}
if (
initNode.object?.type === "MemberExpression" &&
initNode.object.object?.type === "Identifier" &&
getMemberExpressionPropertyName(initNode.object.property) ===
"instance" &&
getMemberExpressionPropertyName(initNode.property) === "exports"
) {
const wasmSource = wasmResultByVarName.get(
initNode.object.object.name,
);
if (wasmSource) {
setSyntheticImportRef(
allImports,
allExports,
src,
file,
wasmSource,
exportNames,
path.node.loc?.start,
);
}
}
if (
initNode.object?.type === "Identifier" &&
getMemberExpressionPropertyName(initNode.property) === "exports"
) {
const wasmSource = wasmInstanceByVarName.get(initNode.object.name);
if (wasmSource) {
setSyntheticImportRef(
allImports,
allExports,
src,
file,
wasmSource,
exportNames,
path.node.loc?.start,
);
}
}
}
}
if (
idNode.type === "Identifier" &&
initNode.type === "MemberExpression" &&
initNode.object?.type === "Identifier" &&
getMemberExpressionPropertyName(initNode.property) === "instance"
) {
const wasmSource = wasmResultByVarName.get(initNode.object.name);
if (wasmSource) {
wasmInstanceByVarName.set(idNode.name, wasmSource);
}
}
if (
idNode.type === "Identifier" &&
initNode.type === "CallExpression" &&
initNode.callee?.type === "MemberExpression" &&
initNode.callee.object?.type === "Identifier" &&
initNode.callee.object.name === "WebAssembly"
) {
const wasmSource = getWasmSourceFromInstantiateCall(
initNode,
wasmBufferByVarName,
);
if (wasmSource) {
wasmResultByVarName.set(idNode.name, wasmSource);
wasmPathLiterals.add(wasmSource);
}
}
},
AssignmentExpression: (path) => {
const wasmExportMemberInfo = getWasmExportMemberInfo(path?.node?.right);
if (!wasmExportMemberInfo?.exportName) {
return;
}
if (!wasmExportAliases.has(wasmExportMemberInfo.aliasName)) {
return;
}
if (!wasmPathLiterals.size) {
return;
}
for (const wasmPath of wasmPathLiterals) {
setSyntheticImportRef(
allImports,
allExports,
src,
file,
wasmPath,
[wasmExportMemberInfo.exportName],
path.node.loc?.start,
);
}
const targetName = getAssignmentTargetName(path?.node?.left);
if (!targetName) {
return;
}
for (const wasmPath of wasmPathLiterals) {
setSyntheticExportRef(
allImports,
allExports,
src,
file,
wasmPath,
[targetName],
path.node.loc?.start,
);
}
},
NewExpression: (path) => {
if (path?.node?.callee?.type === "Identifier") {
if (wasiConstructorAliases.has(path.node.callee.name)) {
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"node:wasi",
["WASI"],
path.node.loc?.start,
);
}
}
if (
path?.node?.callee?.type === "MemberExpression" &&
path.node.callee.object?.type === "Identifier" &&
wasiNamespaceAliases.has(path.node.callee.object.name) &&
getMemberExpressionPropertyName(path.node.callee.property) === "WASI"
) {
setSyntheticImportRef(
allImports,
allExports,
src,
file,
"node:wasi",
["WASI"],
path.node.loc?.start,
);
}
},
ObjectProperty: (path) => {
const keyName = getObjectPropertyKeyName(path?.node?.key);
if (!keyName) {
return;
}
if (ANGULAR_RESOURCE_METADATA_KEYS.has(keyName)) {
for (const stringNode of getAngularStringNodes(path.node.value)) {
setFileRef(allImports, allExports, src, file, stringNode);
}
}
if (ANGULAR_PACKAGE_STRING_KEYS.has(keyName)) {
for (const stringNode of getAngularStringNodes(path.node.value)) {
const reference = getStringValue(stringNode);
if (keyName === "loadChildren" && /^[./]/.test(reference || "")) {
setFileRef(allImports, allExports, src, file, {
value: stripAngularRouteFragment(reference),
loc: stringNode.loc,
});
continue;
}
setAngularPackageRef(
allImports,
allExports,
src,
file,
reference,
stringNode.loc?.start,
);
}
}
},
// Use for export barrells
ExportAllDeclaration: (path) => {
setFileRef(allImports, allExports, src, file, path.node.source);
},
ExportNamedDeclaration: (path) => {
// ensure there is a path export
if (path?.node?.source) {
setFileRef(
allImports,
allExports,
src,
file,
path.node.source,
path.node.specifiers,
);
}
},
});
};
/**
* Return paths to all (j|tsx?) files.
*/
const getAllSrcJSAndTSFiles = (src, deep) =>
Promise.all(
[".js", ".jsx", ".cjs", ".mjs", ".ts", ".tsx", ".vue", ".svelte"].map(
(extension) => {
const searchOptions = normalizeAnalyzerSearchOptions(deep);
return getAllFiles(
searchOptions.deep,
src,
extension,
undefined,
undefined,
undefined,
src,
searchOptions.exclude,
);
},
),
);
const getAngularWorkspaceManifestFiles = (
dir,
deep,
result = [],
rootDir = dir,
excludePatterns = [],
) => {
let files;
try {
files = readdirSync(dir);
} catch {
return result;
}
for (const entry of files) {
if (entry.startsWith(".")) {
continue;
}
const file = join(dir, entry);
let fileStat;
try {
fileStat = lstatSync(file);
} catch {
continue;
}
if (fileStat.isSymbolicLink()) {
continue;
}
if (
shouldExcludeAnalyzerPath(
rootDir,
file,
excludePatterns,
fileStat.isDirectory(),
)
) {
continue;
}
if (fileStat.isDirectory()) {
const dirName = basename(file);
if (
dirName.startsWith("__") ||
IGNORE_DIRS.includes(dirName.toLowerCase()) ||
dirName === "node_modules"
) {
continue;
}
getAngularWorkspaceManifestFiles(
file,
deep,
result,
rootDir,
excludePatterns,
);
continue;
}
if (ANGULAR_WORKSPACE_MANIFEST_NAMES.has(entry)) {
result.push(file);
}
}
return result;
};
const getAngularEvidenceFiles = (
dir,
deep,
result = [],
rootDir = dir,
excludePatterns = [],
) => {
let files;
try {
files = readdirSync(dir);
} catch {
return result;
}
for (const entry of files) {
if (entry.startsWith(".")) {
continue;
}
const file = join(dir, entry);
let fileStat;
try {
fileStat = lstatSync(file);
} catch {
continue;
}
if (fileStat.isSymbolicLink()) {
continue;
}
if (
shouldExcludeAnalyzerPath(
rootDir,
file,
excludePatterns,
fileStat.isDirectory(),
)
) {
continue;
}
if (fileStat.isDirectory()) {
const dirName = basename(file);
if (
dirName.startsWith("__") ||
IGNORE_DIRS.includes(dirName.toLowerCase()) ||
dirName === "node_modules" ||
(!deep && dirName === "dist")
) {
continue;
}
getAngularEvidenceFiles(file, deep, result, rootDir, excludePatterns);
continue;
}
if (
entry === "package.json" ||
ANGULAR_CONFIG_EVIDENCE_FILE_NAMES.has(entry) ||
isAngularTsconfigFileName(entry)
) {
result.push(file);
}
}
return result;
};
const parseAngularJsonFile = (file) => {
try {
return JSON.parse(readFileSync(file, "utf-8"));
} catch {
return undefined;
}
};
const parseAngularJsonLikeFile = (file) => {
try {
const content = readFileSync(file, "utf-8")
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/^\s*\/\/.*$/gm, "");
return JSON.parse(content);
} catch {
return undefined;
}
};
const addAngularBuilderImpliedPackageRefs = (reference, refs) => {
const normalizedReference = String(reference || "").trim();
if (!normalizedReference.includes(":")) {
return;
}
const [builderPackage, builderTarget] = normalizedReference.split(":");
if (
builderPackage === "@angular-devkit/build-angular" ||
builderPackage === "@angular/build"
) {
if (
[
"application",
"browser",
"browser-esbuild",
"dev-server",
"extract-i18n",
"server",
].includes(builderTarget)
) {
refs.add("@angular/compiler-cli");
refs.add("typescript");
}
if (builderTarget === "karma") {
refs.add("karma");
}
if (builderTarget === "protractor") {
refs.add("protractor");
}
}
if (builderPackage === "@angular-devkit/build-ng-packagr") {
refs.add("ng-packagr");
}
if (builderPackage === "@nguniversal/builders") {
refs.add("@angular/compiler-cli");
refs.add("typescript");
}
};
const isAngularPackageSignal = (packageName) =>
packageName === "@angular" ||
packageName?.startsWith("@angular/") ||
packageName?.startsWith("@angular-") ||
packageName === "@nx/angular" ||
packageName === "@schematics/angular";
const collectAngularWorkspacePackageRefs = (node, keyName, refs) => {
if (typeof node === "string") {
if (ANGULAR_WORKSPACE_PACKAGE_STRING_KEYS.has(keyName)) {
const packageName = extractAngularPackageName(node, true);
if (packageName) {
refs.add(packageName);
}
if (keyName === "builder" || keyName === "executor") {
addAngularBuilderImpliedPackageRefs(node, refs);
}
}
return;
}
if (Array.isArray(node)) {
for (const item of node) {
collectAngularWorkspacePackageRefs(item, keyName, refs);
}
return;
}
if (!node || typeof node !== "object") {
return;
}
if (
ANGULAR_WORKSPACE_PACKAGE_STRING_KEYS.has(keyName) &&
typeof node.input === "string"
) {
const packageName = extractAngularPackageName(node.input, true);
if (packageName) {
refs.add(packageName);
}
}
for (const [childKey, childValue] of Object.entries(node)) {
if (typeof childKey === "string" && childKey.includes(":")) {
const packageName = extractAngularPackageName(childKey, true);
if (packageName) {
refs.add(packageName);
}
}
if (
ANGULAR_WORKSPACE_PACKAGE_STRING_KEYS.has(childKey) &&
typeof childValue === "object" &&
!Array.isArray(childValue) &&
typeof childValue?.input === "string"
) {
const packageName = extractAngularPackageName(childValue.input, true);
if (packageName) {
refs.add(packageName);
}
}
collectAngularWorkspacePackageRefs(childValue, childKey, refs);
}
};
const parseAngularWorkspaceManifests = (
src,
allImports,
allExports,
deep,
excludePatterns,
) => {
const manifestFiles = getAngularWorkspaceManifestFiles(
src,
deep,
[],
src,
excludePatterns,
);
const parsedAngularManifestFiles = [];
const hasAngularSourceEvidence = Object.keys(allImports).some((importName) =>
isAngularPackageSignal(importName),
);
for (const manifestFile of manifestFiles) {
const manifest = parseAngularJsonFile(manifestFile);
if (!manifest) {
continue;
}
const packageRefs = new Set();
collectAngularWorkspacePackageRefs(manifest, undefined, packageRefs);
if (
basename(manifestFile) === "project.json" &&
!hasAngularSourceEvidence &&
!Array.from(packageRefs).some((packageRef) =>
isAngularPackageSignal(packageRef),
)
) {
continue;
}
parsedAngularManifestFiles.push(manifestFile);
for (const packageRef of packageRefs) {
setAngularPackageRef(
allImports,
allExports,
src,
manifestFile,
packageRef,
undefined,
);
}
}
return parsedAngularManifestFiles;
};
const hasAngularImportEvidence = (allImports, manifestFiles) =>
manifestFiles.length > 0 ||
Object.keys(allImports).some((importName) =>
isAngularPackageSignal(importName),
);
const parseAngularTemplateFiles = (
src,
allImports,
allExports,
deep,
excludePatterns,
) => {
const templateFiles = getAllFiles(
deep,
src,
".html",
undefined,
undefined,
undefined,
src,
excludePatterns,
);
for (const templateFile of templateFiles) {
let content;
try {
content = readFileSync(templateFile, "utf-8");
} catch {
continue;
}
const lines = content.split(/\r?\n/);
for (const [index, line] of lines.entries()) {
for (const templatePattern of ANGULAR_TEMPLATE_PACKAGE_PATTERNS) {
if (templatePattern.pattern.test(line)) {
setAngularPackageRef(
allImports,
allExports,
src,
templateFile,
templatePattern.packageName,
{ line: index + 1, column: 0 },
);
}
}
}
}
};
const addAngularScriptPackageRefs = (
allImports,
allExports,
src,
file,
scriptValue,
) => {
const script = String(scriptValue || "");
if (!script) {
return;
}
for (const commandPackage of ANGULAR_SCRIPT_COMMAND_PACKAGES) {
if (commandPackage.pattern.test(script)) {
for (const packageName of commandPackage.packages) {
setAngularPackageRef(
allImports,
allExports,
src,
file,
packageName,
undefined,
);
}
}
}
};
const parseAngularPackageJsonScripts = (
src,
allImports,
allExports,
evidenceFiles,
) => {
for (const evidenceFile of evidenceFiles) {
if (basename(evidenceFile) !== "package.json") {
continue;
}
const packageJson = parseAngularJsonFile(evidenceFile);
if (!packageJson?.scripts || typeof packageJson.scripts !== "object") {
continue;
}
for (const scriptValue of Object.values(packageJson.scripts)) {
addAngularScriptPackageRefs(
allImports,
allExports,
src,
evidenceFile,
scriptValue,
);
}
}
};
const addAngularConfigPatternPackageRefs = (
allImports,
allExports,
src,
file,
content,
configPatterns,
) => {
for (const configPattern of configPatterns) {
if (configPattern.pattern.test(content)) {
for (const packageName of configPattern.packages) {
setAngularPackageRef(
allImports,
allExports,
src,
file,
packageName,
undefined,
);
}
}
}
};
const parseAngularToolConfigFiles = (
src,
allImports,
allExports,
evidenceFiles,
) => {
for (const evidenceFile of evidenceFiles) {
const evidenceFileName = basename(evidenceFile);
if (!ANGULAR_CONFIG_EVIDENCE_FILE_NAMES.has(evidenceFileName)) {
continue;
}
let content;
try {
content = readFileSync(evidenceFile, "utf-8");
} catch {
continue;
}
setAngularPackageRef(
allImports,
allExports,
src,
evidenceFile,
evidenceFileName.startsWith("karma.") ? "karma" : "protractor",
undefined,
);
for (const requiredPackage of content.matchAll(
/require\(["']([^"']+)["']\)/g,
)) {
setAngularPackageRef(
allImports,
allExports,
src,
evidenceFile,
requiredPackage[1],
undefined,
);
}
if (evidenceFileName.startsWith("karma.")) {
addAngularConfigPatternPackageRefs(
allImports,
allExports,
src,
evidenceFile,
content,
ANGULAR_KARMA_CONFIG_PATTERNS,
);
continue;
}
addAngularConfigPatternPackageRefs(
allImports,
allExports,
src,
evidenceFile,
content,
ANGULAR_PROTRACTOR_CONFIG_PATTERNS,
);
}
};
const parseAngularTsconfigFiles = (
src,
allImports,
allExports,
evidenceFiles,
) => {
for (const evidenceFile of evidenceFiles) {
if (!isAngularTsconfigFileName(basename(evidenceFile))) {
continue;
}
const tsconfig = parseAngularJsonLikeFile(evidenceFile);
const compilerOptions = tsconfig?.compilerOptions;
if (!compilerOptions || typeof compilerOptions !== "object") {
continue;
}
if (compilerOptions.importHelpers === true) {
setAngularPackageRef(
allImports,
allExports,
src,
evidenceFile,
"tslib",
undefined,
);
}
if (Array.isArray(compilerOptions.types)) {
for (const typeName of compilerOptions.types) {
if (typeof typeName !== "string" || !typeName) {
continue;
}
const typePackageName = typeName.startsWith("@")
? typeName
: `@types/${typeName}`;
setAngularPackageRef(
allImports,
allExports,
src,
evidenceFile,
typePackageName,
undefined,
);
}
}
if (Array.isArray(compilerOptions.plugins)) {
for (const plugin of compilerOptions.plugins) {
const pluginReference =
plugin?.transform || plugin?.name || plugin?.import;
if (typeof pluginReference === "string") {
setAngularPackageRef(
allImports,
allExports,
src,
evidenceFile,
pluginReference,
undefined,
);
}
}
}
}
};
export const CHROMIUM_EXTENSION_CAPABILITY_CATEGORIES = [
"fileAccess",
"deviceAccess",
"network",
"bluetooth",
"accessibility",
"codeInjection",
"fingerprinting",
];
const EXTENSION_CAPABILITY_CHAIN_PATTERNS = {
fileAccess: [
/^(chrome|browser)\.(downloads|fileSystem|fileBrowserHandler|fileManagerPrivate)\b/i,
/^(window\.)?show(Open|Save|Directory)FilePicker$/i,
],
deviceAccess: [
/^(chrome|browser)\.(usb|hid|serial|nfc|mediaGalleries|gcdPrivate|bluetooth|bluetoothPrivate)\b/i,
],
network: [
/^(chrome|browser)\.(webRequest|declarativeNetRequest|proxy|webNavigation|socket)\b/i,
/^(window\.)?(fetch|WebSocket|EventSource)$/i,
/^(XMLHttpRequest)\b/i,
/^navigator\.sendBeacon$/i,
],
bluetooth: [/^(chrome|browser)\.(bluetooth|bluetoothPrivate)\b/i],
accessibility: [
/^(chrome|browser)\.(accessibilityFeatures|accessibilityPrivate|automation)\b/i,
],
codeInjection: [
/^(chrome|browser)\.(scripting\.executeScript|tabs\.executeScript|userScripts|debugger)\b/i,
/^(window\.)?(eval|Function)$/i,
/^document\.write$/i,
],
fingerprinting: [
/^navigator\.(userAgent|platform|languages|language|hardwareConcurrency|deviceMemory|plugins|userAgentData)\b/i,
/^(screen\.)?(width|height|availWidth|availHeight|colorDepth|pixelDepth)$/i,
/^(window\.)?(AudioContext|OfflineAudioContext|RTCPeerConnection)$/i,
/^(canvas|[a-zA-Z_$][a-zA-Z0-9_$]*