UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

1,880 lines (1,819 loc) 157 kB
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_$]*