cognium-dev 3.104.0 → 3.106.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1031 -51
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -4,8 +4,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/cli.ts
6
6
  import { readFileSync, existsSync } from "fs";
7
- import { stat, readdir } from "fs/promises";
8
- import { join, dirname as dirname2, extname, resolve, relative } from "path";
7
+ import { stat as stat2, readdir as readdir2 } from "fs/promises";
8
+ import { join as join2, dirname as dirname2, extname, resolve, relative as relative3 } from "path";
9
9
  import { createRequire as createRequire2 } from "module";
10
10
 
11
11
  // ../../node_modules/web-tree-sitter/web-tree-sitter.js
@@ -18060,6 +18060,85 @@ function isHighConfidence(finding) {
18060
18060
  return finding.confidence === undefined || finding.confidence === "high";
18061
18061
  }
18062
18062
 
18063
+ // ../circle-ir/dist/analysis/library-api-surface-downgrade.js
18064
+ var LIBRARY_API_SURFACE_TAG = "library-api-surface:caller-responsibility";
18065
+ function applyLibraryApiSurfaceDowngrade(findings) {
18066
+ return findings.map((f) => {
18067
+ if (!f.tags?.includes(LIBRARY_API_SURFACE_TAG))
18068
+ return f;
18069
+ if (f.severity === "medium" || f.severity === "low")
18070
+ return f;
18071
+ return {
18072
+ ...f,
18073
+ original_severity: f.severity,
18074
+ severity: "medium",
18075
+ level: "warning"
18076
+ };
18077
+ });
18078
+ }
18079
+
18080
+ // ../circle-ir/dist/analysis/project-profile-transform.js
18081
+ var DOWNGRADE_ELIGIBLE_RULE_IDS = new Set([
18082
+ "code_injection",
18083
+ "template_injection",
18084
+ "xpath_injection",
18085
+ "sql_injection"
18086
+ ]);
18087
+ function libraryDowngrade(severity) {
18088
+ switch (severity) {
18089
+ case "critical":
18090
+ return { severity: "medium", level: "warning" };
18091
+ case "high":
18092
+ return { severity: "low", level: "note" };
18093
+ case "medium":
18094
+ return { severity: "low", level: "note" };
18095
+ case "low":
18096
+ return { severity: "low", level: "note" };
18097
+ }
18098
+ }
18099
+ function levelForSeverity(severity) {
18100
+ switch (severity) {
18101
+ case "critical":
18102
+ return "error";
18103
+ case "high":
18104
+ return "error";
18105
+ case "medium":
18106
+ return "warning";
18107
+ case "low":
18108
+ return "note";
18109
+ }
18110
+ }
18111
+ function applyProjectProfileTransform(findings, resolveProfile) {
18112
+ return findings.map((f) => {
18113
+ if (!f.tags?.includes(LIBRARY_API_SURFACE_TAG))
18114
+ return f;
18115
+ const profile = resolveProfile(f.file);
18116
+ if (profile === "unknown")
18117
+ return f;
18118
+ const shape = profile.split("/")[0];
18119
+ if (shape === "library") {
18120
+ if (!DOWNGRADE_ELIGIBLE_RULE_IDS.has(f.rule_id))
18121
+ return f;
18122
+ const { severity, level } = libraryDowngrade(f.severity);
18123
+ if (severity === f.severity && level === f.level)
18124
+ return f;
18125
+ return { ...f, severity, level };
18126
+ }
18127
+ if (shape === "application") {
18128
+ if (!f.original_severity)
18129
+ return f;
18130
+ if (f.original_severity === f.severity)
18131
+ return f;
18132
+ return {
18133
+ ...f,
18134
+ severity: f.original_severity,
18135
+ level: levelForSeverity(f.original_severity)
18136
+ };
18137
+ }
18138
+ return f;
18139
+ });
18140
+ }
18141
+
18063
18142
  // ../circle-ir/dist/languages/registry.js
18064
18143
  class DefaultLanguageRegistry {
18065
18144
  plugins = new Map;
@@ -23545,6 +23624,39 @@ var PROTOCOL_CLIENT_PACKAGES = [
23545
23624
  var OS_EXEC_RECEIVER_RE = /\b(?:Runtime|ProcessBuilder|DefaultExecutor|Executor|Exec|Launcher|ProcStarter|ProcessExecutor|RuntimeUtil)\s*[.(]/;
23546
23625
  var PROCESS_BUILDER_ARGV_FORM_RE = /\bnew\s+ProcessBuilder\s*\(\s*(?:Arrays\.asList\b|List\.of\b|Collections\.singletonList\b|new\s+ArrayList\b|new\s+String\s*\[\s*\]\s*\{|"[^"]*"\s*,)/;
23547
23626
  var JAVA_THROW_STATEMENT_RE = /^\s*throw\s+new\s+\w+(?:Exception|Error)\b/;
23627
+ var JEXL_ENGINE_TYPES = new Set(["JexlEngine", "Jexl", "JxltEngine"]);
23628
+ var JEXL_EXPRESSION_TYPE_RE = /(?:Jexl)?(?:Expression|Script|Template)(?:Script)?$/;
23629
+ var TEMPLATE_COMPILE_RECEIVER_TYPES = new Set([
23630
+ "Handlebars",
23631
+ "Mustache",
23632
+ "MustacheFactory",
23633
+ "DefaultMustacheFactory",
23634
+ "Pebble",
23635
+ "PebbleEngine",
23636
+ "PebbleEngineBuilder",
23637
+ "VelocityEngine",
23638
+ "Velocity",
23639
+ "Configuration",
23640
+ "Freemarker",
23641
+ "FreeMarker",
23642
+ "TemplateEngine",
23643
+ "SpringTemplateEngine",
23644
+ "Thymeleaf"
23645
+ ]);
23646
+ var SPI_GET_RESOURCES_RE = /\.getResources\s*\(\s*["'][^"']*META-INF\/services\/[^"']*["']\s*\)/;
23647
+ var CLASSLOADER_PARENT_TYPES = new Set([
23648
+ "ClassLoader",
23649
+ "URLClassLoader",
23650
+ "SecureClassLoader"
23651
+ ]);
23652
+ var CLASSLOADER_NAME_RE = /(?:CachingProvider|ClassLoader|SpiLoader)$/;
23653
+ var LOADCLASS_OVERRIDE_METHOD_RE = /\b(?:public|protected)\s+(?:[\w<>?,\s]+\s+)?(?:loadClass|findClass)\s*\(\s*String\b/;
23654
+ var JAVA_CLASS_DECL_RE = /\bclass\s+([A-Z]\w*)\b(?:\s+extends\s+([A-Z][\w.]*))?(?:\s+implements\s+([A-Z][\w.,<>\s?]*))?/;
23655
+ var SQL_BUILDER_CLASS_RE = /(?:Dialect|SqlBuilder|Quoter|Wrapper|SqlGenerator|QueryBuilder)$/;
23656
+ var SQL_BUILDER_WRAPPER_CALL_RE = /\.(?:wrap|quote|escape|identifier)\s*\(/;
23657
+ var SQL_EXTRACTION_RETURN_RE = /^(?:String|CharSequence|Optional\s*<\s*String\s*>)$/;
23658
+ var SQL_EXTRACTION_NAME_RE = /^(?:get|extract|build).*(?:[Ss]ql|[Qq]uery)|^toSql|.*Statement.*ToString$|.*Query.*String$/;
23659
+ var SQL_EXTRACTION_PRIMITIVE_INPUT_RE = /^(?:String|CharSequence)$/;
23548
23660
  function resolveJavaReceiverType(receiver, sinkLine, sourceLines) {
23549
23661
  if (!receiver || !/^[A-Za-z_]\w*$/.test(receiver))
23550
23662
  return null;
@@ -23561,6 +23673,48 @@ function resolveJavaReceiverType(receiver, sinkLine, sourceLines) {
23561
23673
  }
23562
23674
  return null;
23563
23675
  }
23676
+ function findEnclosingClassDecl(sinkLine, sourceLines) {
23677
+ const end = Math.min(sourceLines.length, sinkLine);
23678
+ for (let i2 = end - 1;i2 >= 0; i2--) {
23679
+ const ln = sourceLines[i2] ?? "";
23680
+ const m = ln.match(JAVA_CLASS_DECL_RE);
23681
+ if (!m)
23682
+ continue;
23683
+ const implementsTypes = m[3] ? m[3].split(",").map((s) => s.replace(/<[^>]*>/g, "").trim()).filter(Boolean).map((s) => {
23684
+ const parts2 = s.split(".");
23685
+ return parts2[parts2.length - 1] ?? s;
23686
+ }) : [];
23687
+ const extendsRaw = m[2] ?? null;
23688
+ const extendsType = extendsRaw ? (() => {
23689
+ const p = extendsRaw.split(".");
23690
+ return p[p.length - 1] ?? extendsRaw;
23691
+ })() : null;
23692
+ return { name: m[1], extendsType, implementsTypes };
23693
+ }
23694
+ return null;
23695
+ }
23696
+ function isInsideLoadClassOverride(sinkLine, sourceLines) {
23697
+ const limit = Math.max(0, sinkLine - 200);
23698
+ for (let i2 = sinkLine - 2;i2 >= limit; i2--) {
23699
+ const ln = sourceLines[i2] ?? "";
23700
+ if (LOADCLASS_OVERRIDE_METHOD_RE.test(ln))
23701
+ return true;
23702
+ if (/\bclass\s+[A-Z]\w*\b/.test(ln))
23703
+ return false;
23704
+ }
23705
+ return false;
23706
+ }
23707
+ function findEnclosingMethodFromIr(types, sinkLine) {
23708
+ for (const t of types) {
23709
+ if (sinkLine < t.start_line || sinkLine > t.end_line)
23710
+ continue;
23711
+ for (const m of t.methods) {
23712
+ if (sinkLine >= m.start_line && sinkLine <= m.end_line)
23713
+ return m;
23714
+ }
23715
+ }
23716
+ return null;
23717
+ }
23564
23718
  function isJavaLiteralOrAnnotationAccessor(expr) {
23565
23719
  const e = expr.trim();
23566
23720
  if (e === "")
@@ -23842,6 +23996,168 @@ class SinkFilterPass {
23842
23996
  return true;
23843
23997
  });
23844
23998
  }
23999
+ if (language === "java") {
24000
+ const sourceLines = ctx.code.split(`
24001
+ `);
24002
+ for (const sink of filtered) {
24003
+ if (sink.type !== "code_injection")
24004
+ continue;
24005
+ const sinkLineText = sourceLines[sink.line - 1] ?? "";
24006
+ const receiverMatch = sinkLineText.match(/\b(\w+)\s*\.\s*(\w+)\s*\(/);
24007
+ const receiver = receiverMatch?.[1];
24008
+ const method = sink.method ?? receiverMatch?.[2];
24009
+ let shouldTag = false;
24010
+ if (method === "createExpression" && receiver) {
24011
+ const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
24012
+ if (recvType && JEXL_ENGINE_TYPES.has(recvType))
24013
+ shouldTag = true;
24014
+ }
24015
+ if (!shouldTag && method === "evaluate" && receiver) {
24016
+ const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
24017
+ if (recvType && JEXL_EXPRESSION_TYPE_RE.test(recvType))
24018
+ shouldTag = true;
24019
+ }
24020
+ if (!shouldTag && method === "compile" && receiver) {
24021
+ const recvType = resolveJavaReceiverType(receiver, sink.line, sourceLines);
24022
+ if (recvType && TEMPLATE_COMPILE_RECEIVER_TYPES.has(recvType))
24023
+ shouldTag = true;
24024
+ }
24025
+ if (!shouldTag && method === "forName") {
24026
+ const start2 = Math.max(0, sink.line - 31);
24027
+ const end = Math.min(sourceLines.length, sink.line + 30);
24028
+ for (let i2 = start2;i2 < end; i2++) {
24029
+ if (SPI_GET_RESOURCES_RE.test(sourceLines[i2] ?? "")) {
24030
+ shouldTag = true;
24031
+ break;
24032
+ }
24033
+ }
24034
+ }
24035
+ if (!shouldTag && method && (method === "loadClass" || method === "findClass" || method === "forName")) {
24036
+ const enclosing = findEnclosingClassDecl(sink.line, sourceLines);
24037
+ if (enclosing) {
24038
+ const extendsClassLoader = enclosing.extendsType !== null && CLASSLOADER_PARENT_TYPES.has(enclosing.extendsType);
24039
+ const implementsSpi = enclosing.implementsTypes.some((t) => /CachingProvider$/.test(t));
24040
+ const nameLooksLikeLoader = CLASSLOADER_NAME_RE.test(enclosing.name);
24041
+ if (extendsClassLoader || implementsSpi || nameLooksLikeLoader)
24042
+ shouldTag = true;
24043
+ }
24044
+ if (!shouldTag && isInsideLoadClassOverride(sink.line, sourceLines)) {
24045
+ shouldTag = true;
24046
+ }
24047
+ }
24048
+ if (shouldTag) {
24049
+ const existing = sink.tags ?? [];
24050
+ if (!existing.includes(LIBRARY_API_SURFACE_TAG)) {
24051
+ sink.tags = [...existing, LIBRARY_API_SURFACE_TAG];
24052
+ }
24053
+ }
24054
+ }
24055
+ }
24056
+ if (language === "java") {
24057
+ const sourceLines = ctx.code.split(`
24058
+ `);
24059
+ filtered = filtered.filter((sink) => {
24060
+ if (sink.type !== "code_injection")
24061
+ return true;
24062
+ const sinkLineText = sourceLines[sink.line - 1] ?? "";
24063
+ const receiverMatch = sinkLineText.match(/\b(\w+)\s*\.\s*(\w+)\s*\(/);
24064
+ const receiver = receiverMatch?.[1];
24065
+ if (!receiver)
24066
+ return true;
24067
+ const start2 = Math.max(0, sink.line - 200);
24068
+ let arrayName = null;
24069
+ const foreachRe = new RegExp(`\\bfor\\s*\\(\\s*\\w[\\w<>?]*\\s+${receiver}\\s*:\\s*(\\w+)\\b`);
24070
+ for (let i2 = sink.line - 1;i2 >= start2; i2--) {
24071
+ const ln = sourceLines[i2] ?? "";
24072
+ const fm = ln.match(foreachRe);
24073
+ if (fm) {
24074
+ arrayName = fm[1];
24075
+ break;
24076
+ }
24077
+ }
24078
+ if (!arrayName) {
24079
+ const methodNameMatch = sinkLineText.match(/\.(\w+)\s*\(/);
24080
+ const methodNameRe = methodNameMatch ? methodNameMatch[1] : "\\w+";
24081
+ const idxRe = new RegExp(`\\b(\\w+)\\s*\\[[^\\]]+\\]\\s*\\.\\s*${methodNameRe}\\s*\\(`);
24082
+ const im = sinkLineText.match(idxRe);
24083
+ if (im)
24084
+ arrayName = im[1];
24085
+ }
24086
+ if (!arrayName)
24087
+ return true;
24088
+ const declRe = new RegExp(`\\bprivate\\s+static\\s+final\\s+(?:[\\w<>?,\\s]+)\\[\\s*\\]\\s+${arrayName}\\s*=`);
24089
+ let declStart = -1;
24090
+ for (let i2 = 0;i2 < sink.line; i2++) {
24091
+ if (declRe.test(sourceLines[i2] ?? "")) {
24092
+ declStart = i2;
24093
+ break;
24094
+ }
24095
+ }
24096
+ if (declStart < 0)
24097
+ return true;
24098
+ let body2 = "";
24099
+ for (let i2 = declStart;i2 < Math.min(sourceLines.length, declStart + 40); i2++) {
24100
+ body2 += (sourceLines[i2] ?? "") + `
24101
+ `;
24102
+ if (/\}\s*;/.test(sourceLines[i2] ?? ""))
24103
+ break;
24104
+ }
24105
+ const initMatch = body2.match(/=\s*(?:new\s+[\w<>?,\s]+\[\s*\]\s*)?\{([\s\S]*?)\}\s*;/);
24106
+ if (!initMatch)
24107
+ return true;
24108
+ const elements = initMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
24109
+ if (elements.length === 0)
24110
+ return true;
24111
+ const allNewLiterals = elements.every((e) => /^new\s+[A-Z]\w*\s*\([^)]*\)$/.test(e));
24112
+ if (allNewLiterals)
24113
+ return false;
24114
+ return true;
24115
+ });
24116
+ }
24117
+ if (language === "java") {
24118
+ const sourceLines = ctx.code.split(`
24119
+ `);
24120
+ filtered = filtered.filter((sink) => {
24121
+ if (sink.type !== "sql_injection")
24122
+ return true;
24123
+ const enclosing = findEnclosingClassDecl(sink.line, sourceLines);
24124
+ if (!enclosing)
24125
+ return true;
24126
+ if (!SQL_BUILDER_CLASS_RE.test(enclosing.name))
24127
+ return true;
24128
+ const lo = Math.max(0, sink.line - 11);
24129
+ const hi = Math.min(sourceLines.length, sink.line + 10);
24130
+ for (let i2 = lo;i2 < hi; i2++) {
24131
+ if (SQL_BUILDER_WRAPPER_CALL_RE.test(sourceLines[i2] ?? ""))
24132
+ return false;
24133
+ }
24134
+ return true;
24135
+ });
24136
+ }
24137
+ if (language === "java") {
24138
+ const types = graph.ir.types ?? [];
24139
+ if (types.length > 0) {
24140
+ filtered = filtered.filter((sink) => {
24141
+ if (sink.type !== "sql_injection")
24142
+ return true;
24143
+ const enclosing = findEnclosingMethodFromIr(types, sink.line);
24144
+ if (!enclosing)
24145
+ return true;
24146
+ const ret = (enclosing.return_type ?? "").replace(/\s+/g, " ").trim();
24147
+ if (!SQL_EXTRACTION_RETURN_RE.test(ret))
24148
+ return true;
24149
+ if (!SQL_EXTRACTION_NAME_RE.test(enclosing.name))
24150
+ return true;
24151
+ const firstParamType = enclosing.parameters[0]?.type ?? null;
24152
+ if (firstParamType === null)
24153
+ return true;
24154
+ const simpleType = firstParamType.replace(/<[^>]*>/g, "").trim();
24155
+ if (SQL_EXTRACTION_PRIMITIVE_INPUT_RE.test(simpleType))
24156
+ return true;
24157
+ return false;
24158
+ });
24159
+ }
24160
+ }
23845
24161
  return { sources, sinks: filtered, sanitizers };
23846
24162
  }
23847
24163
  }
@@ -34684,6 +35000,13 @@ function getNodeTypesForLanguage(language) {
34684
35000
  ]);
34685
35001
  }
34686
35002
  }
35003
+ function makeProfileResolver(p) {
35004
+ if (p === undefined)
35005
+ return () => "unknown";
35006
+ if (typeof p === "string")
35007
+ return () => p;
35008
+ return (file) => p.get(file) ?? "unknown";
35009
+ }
34687
35010
  async function analyze(code, filePath, language, options = {}) {
34688
35011
  if (!initialized) {
34689
35012
  await initAnalyzer(options);
@@ -34862,6 +35185,21 @@ async function analyze(code, filePath, language, options = {}) {
34862
35185
  flows: interProc.additionalFlows,
34863
35186
  interprocedural: interProc.interprocedural
34864
35187
  };
35188
+ if (taint.flows && taint.flows.length > 0 && taint.sinks.length > 0) {
35189
+ const sinkTagsByKey = new Map;
35190
+ for (const s of taint.sinks) {
35191
+ if (!s.tags || s.tags.length === 0)
35192
+ continue;
35193
+ sinkTagsByKey.set(`${s.line}:${s.type}`, s.tags);
35194
+ }
35195
+ if (sinkTagsByKey.size > 0) {
35196
+ for (const f of taint.flows) {
35197
+ const tags = sinkTagsByKey.get(`${f.sink_line}:${f.sink_type}`);
35198
+ if (tags && !f.tags)
35199
+ f.tags = [...tags];
35200
+ }
35201
+ }
35202
+ }
34865
35203
  const unresolved = detectUnresolved(calls, types, dfg);
34866
35204
  const enriched = buildEnriched(types, calls, taint.sources, taint.sinks);
34867
35205
  const metricValues = new MetricRunner().run({ meta, types, calls, cfg, dfg, taint, imports, exports, unresolved, enriched }, code, language);
@@ -34874,7 +35212,9 @@ async function analyze(code, filePath, language, options = {}) {
34874
35212
  });
34875
35213
  emitFindingsInstrumentation(filePath, findings, taint);
34876
35214
  const verifiedFindings = applyConfidenceFilter(findings, options.includeSpeculative === true);
34877
- const cappedFindings = applyPerFileFindingCap(filePath, verifiedFindings, options.perFileFindingCap ?? DEFAULT_PER_FILE_FINDING_CAP);
35215
+ const downgradedFindings = applyLibraryApiSurfaceDowngrade(verifiedFindings);
35216
+ const profiledFindings = applyProjectProfileTransform(downgradedFindings, makeProfileResolver(options.projectProfile));
35217
+ const cappedFindings = applyPerFileFindingCap(filePath, profiledFindings, options.perFileFindingCap ?? DEFAULT_PER_FILE_FINDING_CAP);
34878
35218
  return {
34879
35219
  meta,
34880
35220
  types,
@@ -35032,6 +35372,491 @@ function deriveProjectRoot(paths) {
35032
35372
  }
35033
35373
  return common.join("/") || "/";
35034
35374
  }
35375
+ // src/project-profile-detect/index.ts
35376
+ import { relative as relative2 } from "path";
35377
+
35378
+ // src/project-profile-detect/walk.ts
35379
+ import { readdir, readFile, stat } from "fs/promises";
35380
+ import { join, relative } from "path";
35381
+
35382
+ // src/project-profile-detect/maven-parse.ts
35383
+ var TAG = (name2) => new RegExp(`<${name2}\\b[^>]*>([\\s\\S]*?)<\\/${name2}>`, "i");
35384
+ var ALL_TAGS = (name2) => new RegExp(`<${name2}\\b[^>]*>([\\s\\S]*?)<\\/${name2}>`, "gi");
35385
+ function firstTag(xml, name2) {
35386
+ const m = TAG(name2).exec(xml);
35387
+ return m?.[1].trim();
35388
+ }
35389
+ function allTags(xml, name2) {
35390
+ const out2 = [];
35391
+ let m;
35392
+ const re = ALL_TAGS(name2);
35393
+ while ((m = re.exec(xml)) !== null)
35394
+ out2.push(m[1].trim());
35395
+ return out2;
35396
+ }
35397
+ var MAVEN_PLUGIN_MAP = {
35398
+ "spring-boot-maven-plugin": "spring-boot",
35399
+ "maven-plugin-plugin": "maven-plugin",
35400
+ "maven-war-plugin": "war",
35401
+ "maven-ear-plugin": "ear",
35402
+ "maven-shade-plugin": "application",
35403
+ "exec-maven-plugin": "application",
35404
+ "maven-assembly-plugin": "application"
35405
+ };
35406
+ function parseMavenPom(xml, moduleRoot, buildFile, directorySignals) {
35407
+ const stripped = xml.replace(/<parent\b[\s\S]*?<\/parent>/i, "");
35408
+ const groupId = firstTag(stripped, "groupId");
35409
+ const artifactId = firstTag(stripped, "artifactId");
35410
+ const version = firstTag(stripped, "version");
35411
+ const packaging = firstTag(stripped, "packaging");
35412
+ const buildBlock = firstTag(xml, "build") ?? "";
35413
+ const pluginBlocks = allTags(buildBlock, "plugin");
35414
+ const plugins = new Set;
35415
+ for (const p of pluginBlocks) {
35416
+ const aid = firstTag(p, "artifactId");
35417
+ if (aid && MAVEN_PLUGIN_MAP[aid])
35418
+ plugins.add(MAVEN_PLUGIN_MAP[aid]);
35419
+ }
35420
+ if (/<parent\b[\s\S]*?<artifactId>\s*spring-boot-starter-parent\s*<\/artifactId>/i.test(xml)) {
35421
+ plugins.add("spring-boot");
35422
+ }
35423
+ if (packaging === "war")
35424
+ plugins.add("war");
35425
+ if (packaging === "ear")
35426
+ plugins.add("ear");
35427
+ if (packaging === "maven-plugin")
35428
+ plugins.add("maven-plugin");
35429
+ const distBlock = firstTag(xml, "distributionManagement") ?? "";
35430
+ const urls = [
35431
+ ...allTags(distBlock, "url")
35432
+ ].map((u) => u.trim()).filter(Boolean);
35433
+ const signals = {
35434
+ ...directorySignals,
35435
+ plugins: [...directorySignals.plugins, ...plugins],
35436
+ packaging,
35437
+ distributionUrls: [...directorySignals.distributionUrls, ...urls]
35438
+ };
35439
+ return {
35440
+ root: moduleRoot,
35441
+ buildSystem: "maven",
35442
+ buildFile,
35443
+ groupId,
35444
+ artifactId,
35445
+ version,
35446
+ signals
35447
+ };
35448
+ }
35449
+
35450
+ // src/project-profile-detect/gradle-parse.ts
35451
+ var GRADLE_PLUGIN_MAP = {
35452
+ "org.springframework.boot": "spring-boot",
35453
+ "io.spring.dependency-management": "spring-boot",
35454
+ "java-library": "java-library",
35455
+ application: "application",
35456
+ war: "war",
35457
+ ear: "ear",
35458
+ "maven-publish": "maven-publish",
35459
+ "java-gradle-plugin": "gradle-plugin",
35460
+ "com.gradle.plugin-publish": "gradle-plugin"
35461
+ };
35462
+ var PLUGIN_ID_RE = /\bid\s*[(\s]\s*['"]([^'"]+)['"]/g;
35463
+ var APPLY_PLUGIN_RE = /\bapply\s+plugin\s*:\s*['"]([^'"]+)['"]/g;
35464
+ var GROUP_RE = /\bgroup\s*[=(]\s*['"]([^'"]+)['"]/;
35465
+ var VERSION_RE = /\bversion\s*[=(]\s*['"]([^'"]+)['"]/;
35466
+ var URL_RE = /\burl\s*[=(]?\s*(?:uri\s*\(\s*)?['"]([^'"]+)['"]/g;
35467
+ function parsePlugins(text) {
35468
+ const ids = new Set;
35469
+ let m;
35470
+ PLUGIN_ID_RE.lastIndex = 0;
35471
+ while ((m = PLUGIN_ID_RE.exec(text)) !== null)
35472
+ ids.add(m[1]);
35473
+ APPLY_PLUGIN_RE.lastIndex = 0;
35474
+ while ((m = APPLY_PLUGIN_RE.exec(text)) !== null)
35475
+ ids.add(m[1]);
35476
+ const out2 = new Set;
35477
+ for (const id of ids) {
35478
+ const tag = GRADLE_PLUGIN_MAP[id];
35479
+ if (tag)
35480
+ out2.add(tag);
35481
+ }
35482
+ return out2;
35483
+ }
35484
+ function parseUrls(text) {
35485
+ const out2 = [];
35486
+ let m;
35487
+ URL_RE.lastIndex = 0;
35488
+ while ((m = URL_RE.exec(text)) !== null)
35489
+ out2.push(m[1]);
35490
+ return out2;
35491
+ }
35492
+ function parseGradleBuild(text, moduleRoot, buildFile, buildSystem, directorySignals) {
35493
+ const cleaned = text.replace(/\/\*[\s\S]*?\*\//g, "");
35494
+ const pluginsSet = parsePlugins(cleaned);
35495
+ const urls = parseUrls(cleaned);
35496
+ const groupId = GROUP_RE.exec(cleaned)?.[1];
35497
+ const version = VERSION_RE.exec(cleaned)?.[1];
35498
+ const artifactId = moduleRoot.split(/[\\/]/).filter(Boolean).pop();
35499
+ const signals = {
35500
+ ...directorySignals,
35501
+ plugins: [...directorySignals.plugins, ...pluginsSet],
35502
+ distributionUrls: [...directorySignals.distributionUrls, ...urls]
35503
+ };
35504
+ return {
35505
+ root: moduleRoot,
35506
+ buildSystem,
35507
+ buildFile,
35508
+ groupId,
35509
+ artifactId,
35510
+ version,
35511
+ signals
35512
+ };
35513
+ }
35514
+
35515
+ // src/project-profile-detect/walk.ts
35516
+ var BUILD_FILES = ["pom.xml", "build.gradle", "build.gradle.kts"];
35517
+ var SKIP_DIRS = new Set([
35518
+ "node_modules",
35519
+ ".git",
35520
+ ".svn",
35521
+ ".hg",
35522
+ "target",
35523
+ "build",
35524
+ "out",
35525
+ "dist",
35526
+ ".gradle",
35527
+ ".idea",
35528
+ ".vscode",
35529
+ "bin",
35530
+ "obj"
35531
+ ]);
35532
+ async function discoverBuildModules(scanRoot) {
35533
+ const modules = [];
35534
+ await walk(scanRoot, modules);
35535
+ return modules;
35536
+ }
35537
+ async function walk(dir, out2) {
35538
+ let entries;
35539
+ try {
35540
+ entries = await readdir(dir);
35541
+ } catch {
35542
+ return;
35543
+ }
35544
+ const buildFile = entries.find((e) => BUILD_FILES.includes(e));
35545
+ if (buildFile) {
35546
+ const buildFilePath = join(dir, buildFile);
35547
+ const buildSystem = buildFile === "pom.xml" ? "maven" : buildFile === "build.gradle.kts" ? "gradle-kts" : "gradle";
35548
+ const signals = await collectDirectorySignals(dir);
35549
+ let mod;
35550
+ try {
35551
+ const raw = await readFile(buildFilePath, "utf-8");
35552
+ mod = buildSystem === "maven" ? parseMavenPom(raw, dir, buildFilePath, signals) : parseGradleBuild(raw, dir, buildFilePath, buildSystem, signals);
35553
+ } catch {
35554
+ mod = { root: dir, buildSystem, buildFile: buildFilePath, signals };
35555
+ }
35556
+ out2.push(mod);
35557
+ }
35558
+ for (const entry of entries) {
35559
+ if (SKIP_DIRS.has(entry) || entry.startsWith("."))
35560
+ continue;
35561
+ const full = join(dir, entry);
35562
+ let s;
35563
+ try {
35564
+ s = await stat(full);
35565
+ } catch {
35566
+ continue;
35567
+ }
35568
+ if (s.isDirectory()) {
35569
+ await walk(full, out2);
35570
+ }
35571
+ }
35572
+ }
35573
+ async function collectDirectorySignals(moduleRoot) {
35574
+ const signals = {
35575
+ plugins: [],
35576
+ distributionUrls: [],
35577
+ hasJpmsModuleInfo: false,
35578
+ hasSpiServices: false,
35579
+ hasMainMethod: false
35580
+ };
35581
+ const javaRoot = join(moduleRoot, "src", "main", "java");
35582
+ const resourcesRoot = join(moduleRoot, "src", "main", "resources");
35583
+ try {
35584
+ await stat(join(javaRoot, "module-info.java"));
35585
+ signals.hasJpmsModuleInfo = true;
35586
+ } catch {}
35587
+ try {
35588
+ const s = await stat(join(resourcesRoot, "META-INF", "services"));
35589
+ if (s.isDirectory())
35590
+ signals.hasSpiServices = true;
35591
+ } catch {}
35592
+ try {
35593
+ signals.hasMainMethod = await scanForMainMethod(javaRoot, 200);
35594
+ } catch {}
35595
+ return signals;
35596
+ }
35597
+ var MAIN_METHOD_RE = /\bpublic\s+static\s+void\s+main\s*\(\s*(?:final\s+)?String\s*(?:\[\s*\]|\.{3})/;
35598
+ async function scanForMainMethod(root, maxFiles) {
35599
+ let visited = 0;
35600
+ const stack = [root];
35601
+ while (stack.length > 0 && visited < maxFiles) {
35602
+ const dir = stack.pop();
35603
+ let entries;
35604
+ try {
35605
+ entries = await readdir(dir);
35606
+ } catch {
35607
+ continue;
35608
+ }
35609
+ for (const e of entries) {
35610
+ const full = join(dir, e);
35611
+ let s;
35612
+ try {
35613
+ s = await stat(full);
35614
+ } catch {
35615
+ continue;
35616
+ }
35617
+ if (s.isDirectory()) {
35618
+ stack.push(full);
35619
+ } else if (s.isFile() && e.endsWith(".java")) {
35620
+ visited++;
35621
+ try {
35622
+ const text = await readFile(full, "utf-8");
35623
+ if (MAIN_METHOD_RE.test(text))
35624
+ return true;
35625
+ } catch {}
35626
+ if (visited >= maxFiles)
35627
+ break;
35628
+ }
35629
+ }
35630
+ }
35631
+ return false;
35632
+ }
35633
+ async function enumerateScanFiles(scanRoot) {
35634
+ const out2 = [];
35635
+ await enumerate(scanRoot, out2);
35636
+ return out2;
35637
+ }
35638
+ async function enumerate(dir, out2) {
35639
+ let entries;
35640
+ try {
35641
+ entries = await readdir(dir);
35642
+ } catch {
35643
+ return;
35644
+ }
35645
+ for (const e of entries) {
35646
+ if (SKIP_DIRS.has(e) || e.startsWith("."))
35647
+ continue;
35648
+ const full = join(dir, e);
35649
+ let s;
35650
+ try {
35651
+ s = await stat(full);
35652
+ } catch {
35653
+ continue;
35654
+ }
35655
+ if (s.isDirectory()) {
35656
+ await enumerate(full, out2);
35657
+ } else if (s.isFile()) {
35658
+ out2.push(full);
35659
+ }
35660
+ }
35661
+ }
35662
+ function ownerOf(file, modules) {
35663
+ let best;
35664
+ let bestLen = -1;
35665
+ for (const m of modules) {
35666
+ if (file === m.root || file.startsWith(m.root + "/")) {
35667
+ if (m.root.length > bestLen) {
35668
+ best = m;
35669
+ bestLen = m.root.length;
35670
+ }
35671
+ }
35672
+ }
35673
+ return best;
35674
+ }
35675
+
35676
+ // src/project-profile-detect/publication-detect.ts
35677
+ var PUBLIC_REGISTRY_HOSTS = new Set([
35678
+ "repo.maven.apache.org",
35679
+ "repo1.maven.org",
35680
+ "oss.sonatype.org",
35681
+ "s01.oss.sonatype.org",
35682
+ "central.sonatype.com",
35683
+ "central.sonatype.org",
35684
+ "plugins.gradle.org",
35685
+ "jcenter.bintray.com"
35686
+ ]);
35687
+ function isPubliclyPublished(urls) {
35688
+ for (const u of urls) {
35689
+ let host;
35690
+ try {
35691
+ host = new URL(u).hostname.toLowerCase();
35692
+ } catch {
35693
+ continue;
35694
+ }
35695
+ if (PUBLIC_REGISTRY_HOSTS.has(host))
35696
+ return true;
35697
+ }
35698
+ return false;
35699
+ }
35700
+
35701
+ // src/project-profile-detect/shape-resolve.ts
35702
+ function resolveShape(mod) {
35703
+ const sig = mod.signals;
35704
+ const has = (tag) => sig.plugins.includes(tag);
35705
+ const reasons = [];
35706
+ if (has("spring-boot")) {
35707
+ reasons.push("spring-boot plugin");
35708
+ return { shape: "server", reasons };
35709
+ }
35710
+ if (has("war") || has("ear")) {
35711
+ reasons.push(has("war") ? "war packaging" : "ear packaging");
35712
+ return { shape: "server", reasons };
35713
+ }
35714
+ if (has("maven-plugin") || has("gradle-plugin")) {
35715
+ reasons.push(has("maven-plugin") ? "maven-plugin packaging" : "gradle-plugin id");
35716
+ return { shape: "plugin", reasons };
35717
+ }
35718
+ if (has("application")) {
35719
+ reasons.push("application plugin");
35720
+ return { shape: "cli", reasons };
35721
+ }
35722
+ if (sig.hasMainMethod) {
35723
+ reasons.push("main(String[]) found");
35724
+ return { shape: "application", reasons };
35725
+ }
35726
+ const libSignals = [];
35727
+ if (has("java-library"))
35728
+ libSignals.push("java-library plugin");
35729
+ if (sig.hasJpmsModuleInfo)
35730
+ libSignals.push("JPMS module-info.java");
35731
+ if (sig.hasSpiServices)
35732
+ libSignals.push("META-INF/services SPI");
35733
+ if (libSignals.length > 0) {
35734
+ const published = isPubliclyPublished(sig.distributionUrls);
35735
+ if (published) {
35736
+ reasons.push(...libSignals, "public-registry distribution");
35737
+ return { shape: "library", reasons };
35738
+ }
35739
+ reasons.push(...libSignals, "no public-registry distribution (internal helper)");
35740
+ return { shape: "application", reasons };
35741
+ }
35742
+ reasons.push("no shape signals");
35743
+ return { shape: "unknown", reasons };
35744
+ }
35745
+
35746
+ // src/project-profile-detect/env-resolve.ts
35747
+ var TEST_RE = /(?:^|\/)tests?\//;
35748
+ var SAMPLE_RE = /(?:^|\/)(?:samples?|examples?|demos?|fixtures?)\//;
35749
+ var BENCHMARK_RE = /(?:^|\/)benchmarks?\//;
35750
+ var PROD_RE = /(?:^|\/)src\/main\//;
35751
+ function resolveEnv(absoluteFile) {
35752
+ const f = absoluteFile.replace(/\\/g, "/").toLowerCase();
35753
+ if (TEST_RE.test(f))
35754
+ return "test";
35755
+ if (BENCHMARK_RE.test(f))
35756
+ return "benchmark";
35757
+ if (SAMPLE_RE.test(f))
35758
+ return "sample";
35759
+ if (PROD_RE.test(f))
35760
+ return "production";
35761
+ return "dev";
35762
+ }
35763
+
35764
+ // src/project-profile-detect/overrides.ts
35765
+ function compileGlob(glob) {
35766
+ let re = "";
35767
+ let i2 = 0;
35768
+ while (i2 < glob.length) {
35769
+ const c = glob[i2];
35770
+ if (c === "*") {
35771
+ if (glob[i2 + 1] === "*") {
35772
+ re += ".*";
35773
+ i2 += 2;
35774
+ if (glob[i2] === "/")
35775
+ i2++;
35776
+ } else {
35777
+ re += "[^/]*";
35778
+ i2++;
35779
+ }
35780
+ } else if (c === "?") {
35781
+ re += "[^/]";
35782
+ i2++;
35783
+ } else if (/[.+^${}()|[\]\\]/.test(c)) {
35784
+ re += "\\" + c;
35785
+ i2++;
35786
+ } else {
35787
+ re += c;
35788
+ i2++;
35789
+ }
35790
+ }
35791
+ return new RegExp("^" + re + "$");
35792
+ }
35793
+ function compileOverrides(overrides) {
35794
+ if (!overrides)
35795
+ return [];
35796
+ return Object.entries(overrides).map(([glob, profile]) => ({
35797
+ re: compileGlob(glob),
35798
+ profile,
35799
+ glob
35800
+ }));
35801
+ }
35802
+ function applyOverrides(relativePath, compiled) {
35803
+ const p = relativePath.replace(/\\/g, "/");
35804
+ for (const { re, profile, glob } of compiled) {
35805
+ if (re.test(p))
35806
+ return { profile, glob };
35807
+ }
35808
+ return;
35809
+ }
35810
+
35811
+ // src/project-profile-detect/index.ts
35812
+ async function detectProjectProfiles(scanRoot, options = {}) {
35813
+ const modules = await discoverBuildModules(scanRoot);
35814
+ const files = await enumerateScanFiles(scanRoot);
35815
+ const compiledOverrides = compileOverrides(options.overrides);
35816
+ const resolvedByRoot = new Map;
35817
+ const resolvedModules = [];
35818
+ for (const m of modules) {
35819
+ const sr = resolveShape(m);
35820
+ const moduleEnv = resolveEnv(m.root);
35821
+ const profile = sr.shape === "unknown" ? "unknown" : `${sr.shape}/${moduleEnv}`;
35822
+ const r = { module: m, profile, reasons: sr.reasons };
35823
+ resolvedByRoot.set(m.root, r);
35824
+ resolvedModules.push(r);
35825
+ }
35826
+ const profileByFile = new Map;
35827
+ const unknownFiles = [];
35828
+ for (const file of files) {
35829
+ const rel = relative2(scanRoot, file);
35830
+ const ov = applyOverrides(rel, compiledOverrides);
35831
+ if (ov) {
35832
+ profileByFile.set(file, ov.profile);
35833
+ continue;
35834
+ }
35835
+ if (options.forcedProfile && options.forcedProfile !== "unknown") {
35836
+ const [shape2] = options.forcedProfile.split("/");
35837
+ const env2 = resolveEnv(file);
35838
+ profileByFile.set(file, `${shape2}/${env2}`);
35839
+ continue;
35840
+ }
35841
+ const owner = ownerOf(file, modules);
35842
+ if (!owner) {
35843
+ profileByFile.set(file, "unknown");
35844
+ unknownFiles.push(file);
35845
+ continue;
35846
+ }
35847
+ const ownerResolution = resolvedByRoot.get(owner.root);
35848
+ if (!ownerResolution || ownerResolution.profile === "unknown") {
35849
+ profileByFile.set(file, "unknown");
35850
+ unknownFiles.push(file);
35851
+ continue;
35852
+ }
35853
+ const [shape] = ownerResolution.profile.split("/");
35854
+ const env = resolveEnv(file);
35855
+ profileByFile.set(file, `${shape}/${env}`);
35856
+ }
35857
+ return { profileByFile, modules: resolvedModules, unknownFiles };
35858
+ }
35859
+
35035
35860
  // src/utils/colors.ts
35036
35861
  var RESET = "\x1B[0m";
35037
35862
  var BOLD = "\x1B[1m";
@@ -35053,9 +35878,17 @@ var colors = {
35053
35878
  };
35054
35879
 
35055
35880
  // src/version.ts
35056
- var version = "3.104.0";
35881
+ var version = "3.106.0";
35057
35882
 
35058
35883
  // src/formatters.ts
35884
+ var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
35885
+ function applyTagSeverityDowngrade(severity, tags) {
35886
+ if (!tags || !tags.includes(LIBRARY_API_SURFACE_TAG2))
35887
+ return severity;
35888
+ if (severity === "critical" || severity === "high")
35889
+ return "medium";
35890
+ return severity;
35891
+ }
35059
35892
  var SINK_SEVERITY = {
35060
35893
  sql_injection: "critical",
35061
35894
  nosql_injection: "high",
@@ -35377,8 +36210,22 @@ function formatCrossFilePaths(taintPaths) {
35377
36210
  return lines.join(`
35378
36211
  `);
35379
36212
  }
35380
- function formatResults(results, verbose, crossFileData) {
36213
+ function formatResults(results, verbose, crossFileData, profileSummary) {
35381
36214
  const lines = [];
36215
+ if (profileSummary && profileSummary.modules.length > 0) {
36216
+ lines.push(colors.bold("Project profile"));
36217
+ for (const m of profileSummary.modules) {
36218
+ const tag = m.root === "" || m.root === "." ? "." : m.root;
36219
+ lines.push(` ${colors.cyan(tag)} → ${colors.bold(m.profile)}`);
36220
+ if (verbose) {
36221
+ lines.push(` reasons: ${m.reasons.join(" → ")}`);
36222
+ }
36223
+ }
36224
+ if (profileSummary.unknownFileCount > 0) {
36225
+ lines.push(colors.dim(` ${profileSummary.unknownFileCount} file(s) outside any module → unknown`));
36226
+ }
36227
+ lines.push("");
36228
+ }
35382
36229
  for (const result of results) {
35383
36230
  if (result.error) {
35384
36231
  lines.push(colors.red(`[ERROR] ${result.file}: ${result.error}`));
@@ -35397,7 +36244,8 @@ function formatResults(results, verbose, crossFileData) {
35397
36244
  const cweTag = vuln.cwe ? ` [${vuln.cwe}]` : "";
35398
36245
  const severityUpper = vuln.severity.charAt(0).toUpperCase() + vuln.severity.slice(1);
35399
36246
  const categoryTag = vuln.category && vuln.category !== "security" ? ` [${vuln.category}]` : "";
35400
- lines.push(` ${colorFn(`[${icon}]`)} ${colorFn(vuln.type)} (${severityUpper})${cweTag}${categoryTag}`);
36247
+ const libraryApiTag = vuln.tags?.includes(LIBRARY_API_SURFACE_TAG2) ? ` ${colors.cyan("[library-api-surface]")}` : "";
36248
+ lines.push(` ${colorFn(`[${icon}]`)} ${colorFn(vuln.type)} (${severityUpper})${cweTag}${categoryTag}${libraryApiTag}`);
35401
36249
  lines.push(` Line ${vuln.line}: ${vuln.message}`);
35402
36250
  const help = VULNERABILITY_HELP[vuln.type];
35403
36251
  const fixText = vuln.fix ?? help?.fix;
@@ -35422,7 +36270,7 @@ function formatResults(results, verbose, crossFileData) {
35422
36270
  return lines.join(`
35423
36271
  `);
35424
36272
  }
35425
- function formatJSON(results, crossFileData) {
36273
+ function formatJSON(results, crossFileData, profileSummary) {
35426
36274
  const output = {
35427
36275
  version,
35428
36276
  timestamp: new Date().toISOString(),
@@ -35434,6 +36282,7 @@ function formatJSON(results, crossFileData) {
35434
36282
  cross_file_taint_paths: crossFileData?.taintPaths ?? [],
35435
36283
  cross_file_calls: crossFileData?.crossFileCalls ?? [],
35436
36284
  cross_file_budget_exceeded: crossFileData?.budgetExceeded ?? false,
36285
+ ...profileSummary ? { project_profile: profileSummary } : {},
35437
36286
  summary: {
35438
36287
  filesScanned: results.length,
35439
36288
  filesWithVulnerabilities: results.filter((r) => r.vulnerabilities.length > 0).length,
@@ -35445,7 +36294,7 @@ function formatJSON(results, crossFileData) {
35445
36294
  };
35446
36295
  return JSON.stringify(output, null, 2);
35447
36296
  }
35448
- function formatSARIF(results, crossFileData) {
36297
+ function formatSARIF(results, crossFileData, profileSummary) {
35449
36298
  const sarif = {
35450
36299
  $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
35451
36300
  version: "2.1.0",
@@ -35459,7 +36308,8 @@ function formatSARIF(results, crossFileData) {
35459
36308
  rules: generateRules(results, crossFileData)
35460
36309
  }
35461
36310
  },
35462
- results: generateSarifResults(results, crossFileData)
36311
+ results: generateSarifResults(results, crossFileData),
36312
+ ...profileSummary ? { properties: { projectProfile: profileSummary } } : {}
35463
36313
  }
35464
36314
  ]
35465
36315
  };
@@ -35522,7 +36372,9 @@ function generateSarifResults(results, crossFileData) {
35522
36372
  properties: {
35523
36373
  cwe: vuln.cwe,
35524
36374
  severity: vuln.severity,
35525
- ...vuln.fix ? { fix: vuln.fix } : {}
36375
+ ...vuln.fix ? { fix: vuln.fix } : {},
36376
+ ...vuln.tags && vuln.tags.length > 0 ? { tags: vuln.tags } : {},
36377
+ ...vuln.profile ? { profile: vuln.profile } : {}
35526
36378
  }
35527
36379
  });
35528
36380
  }
@@ -35632,6 +36484,13 @@ SCAN OPTIONS:
35632
36484
  --disable-pass <passes> Disable specific passes (comma-separated, e.g., "naming-convention,todo-in-prod")
35633
36485
  --exclude-tests Exclude test files and directories
35634
36486
  --profile <file> Load config from file [default: cognium.config.json]
36487
+ --project-profile <p> Force project profile (shape/env). Disables auto-detection.
36488
+ - shape: library|application|cli|server|plugin
36489
+ - env: production|dev|sample|benchmark|test
36490
+ - Example: --project-profile library/production
36491
+ - Default: auto-detect from build files (pom.xml, build.gradle)
36492
+ --no-project-profile Disable project-profile auto-detection (every file → unknown).
36493
+ --project-profile-explain Print detected per-module profiles + reason chain, then exit.
35635
36494
  -o, --output <file> Write results to file
35636
36495
  -q, --quiet Suppress progress output
35637
36496
  -v, --verbose Show detailed output
@@ -35820,7 +36679,7 @@ function applySuppressionsToResults(results, suppressions, basePath) {
35820
36679
  if (suppressions.length === 0)
35821
36680
  return results;
35822
36681
  return results.map((result) => {
35823
- const relativeFile = relative(basePath, result.file) || result.file;
36682
+ const relativeFile = relative3(basePath, result.file) || result.file;
35824
36683
  const filteredVulns = result.vulnerabilities.filter((vuln) => {
35825
36684
  for (const supp of suppressions) {
35826
36685
  if (supp.pass !== vuln.type)
@@ -35903,13 +36762,13 @@ function matchesAnyPattern(filePath, patterns) {
35903
36762
  async function collectFiles(targetPath, options = {}) {
35904
36763
  const { language, excludeTests = false, includePatterns, excludePatterns, basePath } = options;
35905
36764
  const files = [];
35906
- const pathStat = await stat(targetPath);
36765
+ const pathStat = await stat2(targetPath);
35907
36766
  if (pathStat.isFile()) {
35908
36767
  if (excludeTests && isTestFile2(targetPath)) {
35909
36768
  return files;
35910
36769
  }
35911
36770
  if (fileMatchesLanguage(targetPath, language)) {
35912
- const relativePath = basePath ? relative(basePath, targetPath) : targetPath;
36771
+ const relativePath = basePath ? relative3(basePath, targetPath) : targetPath;
35913
36772
  if (includePatterns && includePatterns.length > 0) {
35914
36773
  if (!matchesAnyPattern(relativePath, includePatterns)) {
35915
36774
  return files;
@@ -35921,14 +36780,14 @@ async function collectFiles(targetPath, options = {}) {
35921
36780
  files.push(targetPath);
35922
36781
  }
35923
36782
  } else if (pathStat.isDirectory()) {
35924
- const entries = await readdir(targetPath, { withFileTypes: true });
36783
+ const entries = await readdir2(targetPath, { withFileTypes: true });
35925
36784
  for (const entry of entries) {
35926
36785
  if (entry.name.startsWith(".") || entry.name === "node_modules")
35927
36786
  continue;
35928
36787
  if (excludeTests && /^(test|tests|__tests__|spec|__mocks__)$/i.test(entry.name))
35929
36788
  continue;
35930
- const fullPath = join(targetPath, entry.name);
35931
- const relativePath = basePath ? relative(basePath, fullPath) : fullPath;
36789
+ const fullPath = join2(targetPath, entry.name);
36790
+ const relativePath = basePath ? relative3(basePath, fullPath) : fullPath;
35932
36791
  if (excludePatterns && entry.isDirectory()) {
35933
36792
  const dirPattern = relativePath + "/";
35934
36793
  if (excludePatterns.some((p) => matchesGlob(dirPattern, p) || matchesGlob(relativePath, p))) {
@@ -35945,15 +36804,17 @@ async function scanFile(filePath, language, analyzeOpts) {
35945
36804
  const code = readFileSync(filePath, "utf-8");
35946
36805
  const result = await analyze(code, filePath, language, {
35947
36806
  passOptions: analyzeOpts?.passOptions,
35948
- disabledPasses: analyzeOpts?.disabledPasses
36807
+ disabledPasses: analyzeOpts?.disabledPasses,
36808
+ ...analyzeOpts?.projectProfile !== undefined ? { projectProfile: analyzeOpts.projectProfile } : {}
35949
36809
  });
35950
36810
  const vulnerabilities = (result.taint.flows || []).map((flow) => ({
35951
36811
  type: flow.sink_type,
35952
- severity: SINK_SEVERITY[flow.sink_type] ?? "high",
36812
+ severity: applyTagSeverityDowngrade(SINK_SEVERITY[flow.sink_type] ?? "high", flow.tags),
35953
36813
  message: `${flow.sink_type} vulnerability: tainted data flows from line ${flow.source_line} to line ${flow.sink_line}`,
35954
36814
  line: flow.sink_line,
35955
36815
  cwe: SINK_CWE[flow.sink_type],
35956
- category: "security"
36816
+ category: "security",
36817
+ ...flow.tags && flow.tags.length > 0 ? { tags: [...flow.tags] } : {}
35957
36818
  }));
35958
36819
  for (const finding of result.findings ?? []) {
35959
36820
  vulnerabilities.push({
@@ -35963,7 +36824,8 @@ async function scanFile(filePath, language, analyzeOpts) {
35963
36824
  line: finding.line,
35964
36825
  cwe: finding.cwe,
35965
36826
  fix: finding.fix,
35966
- category: finding.category ?? "reliability"
36827
+ category: finding.category ?? "reliability",
36828
+ ...finding.tags && finding.tags.length > 0 ? { tags: [...finding.tags] } : {}
35967
36829
  });
35968
36830
  }
35969
36831
  return { file: filePath, vulnerabilities };
@@ -35984,16 +36846,18 @@ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
35984
36846
  const projectResult = await analyzeProject(filesWithCode, {
35985
36847
  passOptions: analyzeOpts?.passOptions,
35986
36848
  disabledPasses: analyzeOpts?.disabledPasses,
35987
- ...crossFileBudgetMs !== undefined ? { crossFileBudgetMs } : {}
36849
+ ...crossFileBudgetMs !== undefined ? { crossFileBudgetMs } : {},
36850
+ ...analyzeOpts?.projectProfile !== undefined ? { projectProfile: analyzeOpts.projectProfile } : {}
35988
36851
  });
35989
36852
  const results = projectResult.files.map(({ file, analysis }) => {
35990
36853
  const vulnerabilities = (analysis.taint.flows || []).map((flow) => ({
35991
36854
  type: flow.sink_type,
35992
- severity: SINK_SEVERITY[flow.sink_type] ?? "high",
36855
+ severity: applyTagSeverityDowngrade(SINK_SEVERITY[flow.sink_type] ?? "high", flow.tags),
35993
36856
  message: `${flow.sink_type} vulnerability: tainted data flows from line ${flow.source_line} to line ${flow.sink_line}`,
35994
36857
  line: flow.sink_line,
35995
36858
  cwe: SINK_CWE[flow.sink_type],
35996
- category: "security"
36859
+ category: "security",
36860
+ ...flow.tags && flow.tags.length > 0 ? { tags: [...flow.tags] } : {}
35997
36861
  }));
35998
36862
  for (const finding of analysis.findings ?? []) {
35999
36863
  vulnerabilities.push({
@@ -36003,7 +36867,8 @@ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
36003
36867
  line: finding.line,
36004
36868
  cwe: finding.cwe,
36005
36869
  fix: finding.fix,
36006
- category: finding.category ?? "reliability"
36870
+ category: finding.category ?? "reliability",
36871
+ ...finding.tags && finding.tags.length > 0 ? { tags: [...finding.tags] } : {}
36007
36872
  });
36008
36873
  }
36009
36874
  return { file, vulnerabilities };
@@ -36020,7 +36885,7 @@ async function scanProject(files, language, analyzeOpts, crossFileBudgetMs) {
36020
36885
  async function initWasm(spin) {
36021
36886
  const isStandalone = import.meta.url.includes("/$bunfs/");
36022
36887
  if (isStandalone) {
36023
- const { dirname: dirname3, join: join2 } = await import("path");
36888
+ const { dirname: dirname3, join: join3 } = await import("path");
36024
36889
  const binaryDir = dirname3(process.execPath);
36025
36890
  const cwd = process.cwd();
36026
36891
  let scriptDir = null;
@@ -36031,34 +36896,34 @@ async function initWasm(spin) {
36031
36896
  } catch {}
36032
36897
  }
36033
36898
  const wasmLocations = [
36034
- join2(binaryDir, "wasm"),
36035
- join2(cwd, "wasm"),
36036
- join2(binaryDir, "..", "wasm"),
36899
+ join3(binaryDir, "wasm"),
36900
+ join3(cwd, "wasm"),
36901
+ join3(binaryDir, "..", "wasm"),
36037
36902
  ...scriptDir ? [
36038
- join2(scriptDir, "wasm"),
36039
- join2(scriptDir, "..", "wasm"),
36040
- join2(scriptDir, "..", "node_modules", "circle-ir", "dist", "wasm")
36903
+ join3(scriptDir, "wasm"),
36904
+ join3(scriptDir, "..", "wasm"),
36905
+ join3(scriptDir, "..", "node_modules", "circle-ir", "dist", "wasm")
36041
36906
  ] : []
36042
36907
  ];
36043
36908
  let wasmDir = null;
36044
36909
  for (const location of wasmLocations) {
36045
- if (existsSync(location) && existsSync(join2(location, "web-tree-sitter.wasm"))) {
36910
+ if (existsSync(location) && existsSync(join3(location, "web-tree-sitter.wasm"))) {
36046
36911
  wasmDir = location;
36047
36912
  break;
36048
36913
  }
36049
36914
  }
36050
36915
  if (wasmDir) {
36051
36916
  await initAnalyzer({
36052
- wasmPath: join2(wasmDir, "web-tree-sitter.wasm"),
36917
+ wasmPath: join3(wasmDir, "web-tree-sitter.wasm"),
36053
36918
  languagePaths: {
36054
- bash: join2(wasmDir, "tree-sitter-bash.wasm"),
36055
- go: join2(wasmDir, "tree-sitter-go.wasm"),
36056
- java: join2(wasmDir, "tree-sitter-java.wasm"),
36057
- javascript: join2(wasmDir, "tree-sitter-javascript.wasm"),
36058
- typescript: join2(wasmDir, "tree-sitter-javascript.wasm"),
36059
- python: join2(wasmDir, "tree-sitter-python.wasm"),
36060
- rust: join2(wasmDir, "tree-sitter-rust.wasm"),
36061
- html: join2(wasmDir, "tree-sitter-html.wasm")
36919
+ bash: join3(wasmDir, "tree-sitter-bash.wasm"),
36920
+ go: join3(wasmDir, "tree-sitter-go.wasm"),
36921
+ java: join3(wasmDir, "tree-sitter-java.wasm"),
36922
+ javascript: join3(wasmDir, "tree-sitter-javascript.wasm"),
36923
+ typescript: join3(wasmDir, "tree-sitter-javascript.wasm"),
36924
+ python: join3(wasmDir, "tree-sitter-python.wasm"),
36925
+ rust: join3(wasmDir, "tree-sitter-rust.wasm"),
36926
+ html: join3(wasmDir, "tree-sitter-html.wasm")
36062
36927
  }
36063
36928
  });
36064
36929
  } else {
@@ -36080,7 +36945,7 @@ Please ensure the wasm/ directory is located next to the binary or in your curre
36080
36945
  } else {
36081
36946
  const require2 = createRequire2(import.meta.url);
36082
36947
  const circleIrPkg = require2.resolve("circle-ir/package.json");
36083
- const wasmBasePath = join(dirname2(circleIrPkg), "dist", "wasm") + "/";
36948
+ const wasmBasePath = join2(dirname2(circleIrPkg), "dist", "wasm") + "/";
36084
36949
  await initAnalyzer({
36085
36950
  wasmPath: wasmBasePath + "web-tree-sitter.wasm",
36086
36951
  languagePaths: {
@@ -36096,6 +36961,62 @@ Please ensure the wasm/ directory is located next to the binary or in your curre
36096
36961
  });
36097
36962
  }
36098
36963
  }
36964
+ function buildProfileSummary(scanRoot, modules, resolvedProfiles) {
36965
+ if (!resolvedProfiles)
36966
+ return;
36967
+ if (modules.length === 0 && resolvedProfiles.size === 0)
36968
+ return;
36969
+ let unknownFileCount = 0;
36970
+ for (const p of resolvedProfiles.values()) {
36971
+ if (p === "unknown")
36972
+ unknownFileCount++;
36973
+ }
36974
+ return {
36975
+ scanRoot,
36976
+ modules: modules.map((m) => ({
36977
+ root: relative3(scanRoot, m.module.root) || ".",
36978
+ profile: m.profile,
36979
+ reasons: m.reasons,
36980
+ buildSystem: m.module.buildSystem
36981
+ })),
36982
+ unknownFileCount
36983
+ };
36984
+ }
36985
+ function summarizeModules(modules) {
36986
+ if (modules.length === 0)
36987
+ return "no build files detected";
36988
+ const counts = new Map;
36989
+ for (const m of modules) {
36990
+ counts.set(m.profile, (counts.get(m.profile) ?? 0) + 1);
36991
+ }
36992
+ const parts2 = [...counts.entries()].map(([profile, count]) => `${profile}×${count}`).sort();
36993
+ return `${modules.length} module(s): ${parts2.join(", ")}`;
36994
+ }
36995
+ function printProfileExplain(scanRoot, detection) {
36996
+ const out2 = [];
36997
+ out2.push(colors.bold("Project profile detection"));
36998
+ out2.push(` Scan root: ${scanRoot}`);
36999
+ out2.push(` Modules: ${detection.modules.length}`);
37000
+ out2.push(` Unknown files (no enclosing module): ${detection.unknownFiles.length}`);
37001
+ out2.push("");
37002
+ if (detection.modules.length === 0) {
37003
+ out2.push(" (no pom.xml, build.gradle, or build.gradle.kts found)");
37004
+ } else {
37005
+ for (const r of detection.modules) {
37006
+ const rel = relative3(scanRoot, r.module.root) || ".";
37007
+ out2.push(` ${colors.cyan(rel || ".")} → ${colors.bold(r.profile)}`);
37008
+ out2.push(` build: ${r.module.buildSystem} (${relative3(scanRoot, r.module.buildFile)})`);
37009
+ if (r.module.artifactId) {
37010
+ out2.push(` coords: ${r.module.groupId ?? "?"}:${r.module.artifactId}:${r.module.version ?? "?"}`);
37011
+ }
37012
+ out2.push(` reasons: ${r.reasons.join(" → ")}`);
37013
+ out2.push("");
37014
+ }
37015
+ }
37016
+ process.stderr.write(out2.join(`
37017
+ `) + `
37018
+ `);
37019
+ }
36099
37020
  async function runScan(targetPath, options) {
36100
37021
  const spin = options.quiet ? null : spinner("Initializing analyzer...").start();
36101
37022
  const config = loadConfig(options.profile);
@@ -36149,8 +37070,39 @@ async function runScan(targetPath, options) {
36149
37070
  }
36150
37071
  let results;
36151
37072
  let crossFileData;
36152
- const analyzeOpts = { passOptions, disabledPasses };
36153
- if ((await stat(absPath)).isDirectory()) {
37073
+ const forcedProfile = options.projectProfile ?? config?.profile;
37074
+ const profileOverrides = config?.profileOverrides;
37075
+ const detectionEnabled = !options.noProjectProfile && (await stat2(absPath)).isDirectory();
37076
+ let resolvedProfiles;
37077
+ let detectedModules = [];
37078
+ if (detectionEnabled) {
37079
+ if (spin)
37080
+ spin.text = "Detecting project profile...";
37081
+ const detection = await detectProjectProfiles(absPath, {
37082
+ forcedProfile,
37083
+ overrides: profileOverrides
37084
+ });
37085
+ resolvedProfiles = detection.profileByFile;
37086
+ detectedModules = detection.modules;
37087
+ if (options.projectProfileExplain) {
37088
+ if (spin)
37089
+ spin.stop();
37090
+ printProfileExplain(absPath, detection);
37091
+ return;
37092
+ }
37093
+ if (!options.quiet && detectedModules.length > 0) {
37094
+ const summary = summarizeModules(detectedModules);
37095
+ console.error(colors.dim(`Project profile: ${summary}`));
37096
+ }
37097
+ } else if (forcedProfile && !options.noProjectProfile) {
37098
+ resolvedProfiles = undefined;
37099
+ }
37100
+ const analyzeOpts = {
37101
+ passOptions,
37102
+ disabledPasses,
37103
+ ...resolvedProfiles ? { projectProfile: resolvedProfiles } : forcedProfile && !options.noProjectProfile ? { projectProfile: forcedProfile } : {}
37104
+ };
37105
+ if ((await stat2(absPath)).isDirectory()) {
36154
37106
  if (spin)
36155
37107
  spin.text = `Running project analysis on ${files.length} file(s)...`;
36156
37108
  const projectScan = await scanProject(files, options.language, analyzeOpts, options.crossFileBudgetMs);
@@ -36162,7 +37114,7 @@ async function runScan(targetPath, options) {
36162
37114
  results = [];
36163
37115
  let processed = 0;
36164
37116
  const formatCurrentFile = (file) => {
36165
- const rel = relative(absPath, file) || file;
37117
+ const rel = relative3(absPath, file) || file;
36166
37118
  return rel.length > 80 ? `...${rel.slice(-77)}` : rel;
36167
37119
  };
36168
37120
  const concurrency = options.threads;
@@ -36260,17 +37212,27 @@ async function runScan(targetPath, options) {
36260
37212
  const errors = results.filter((r) => r.error).length;
36261
37213
  const crossFilePaths = crossFileData?.taintPaths.length ?? 0;
36262
37214
  const shouldOutput = totalVulns > 0 || crossFilePaths > 0 || errors > 0 || options.verbose || options.output || options.format !== "text";
37215
+ const profileSummary = buildProfileSummary(absPath, detectedModules, resolvedProfiles);
37216
+ if (resolvedProfiles) {
37217
+ for (const r of results) {
37218
+ const p = resolvedProfiles.get(r.file);
37219
+ if (!p || p === "unknown")
37220
+ continue;
37221
+ for (const v of r.vulnerabilities)
37222
+ v.profile = p;
37223
+ }
37224
+ }
36263
37225
  if (shouldOutput) {
36264
37226
  let output;
36265
37227
  switch (options.format) {
36266
37228
  case "json":
36267
- output = formatJSON(results, crossFileData);
37229
+ output = formatJSON(results, crossFileData, profileSummary);
36268
37230
  break;
36269
37231
  case "sarif":
36270
- output = formatSARIF(results, crossFileData);
37232
+ output = formatSARIF(results, crossFileData, profileSummary);
36271
37233
  break;
36272
37234
  default:
36273
- output = formatResults(results, options.verbose, crossFileData);
37235
+ output = formatResults(results, options.verbose, crossFileData, profileSummary);
36274
37236
  }
36275
37237
  if (options.output) {
36276
37238
  const { writeFileSync } = await import("fs");
@@ -36356,7 +37318,7 @@ async function runMetrics(targetPath, options) {
36356
37318
  continue;
36357
37319
  }
36358
37320
  if (spin) {
36359
- const rel = relative(absPath, file) || file;
37321
+ const rel = relative3(absPath, file) || file;
36360
37322
  const maxLen = 80;
36361
37323
  const label = rel.length > maxLen ? `...${rel.slice(-(maxLen - 3))}` : rel;
36362
37324
  spin.text = `Analyzing ${label}... (${processed}/${totalFiles})`;
@@ -36401,7 +37363,7 @@ async function runMetrics(targetPath, options) {
36401
37363
  } else {
36402
37364
  const lines = [];
36403
37365
  for (const fm of filtered) {
36404
- const rel = relative(absPath, fm.file) || fm.file;
37366
+ const rel = relative3(absPath, fm.file) || fm.file;
36405
37367
  lines.push(rel);
36406
37368
  const byCategory = new Map;
36407
37369
  for (const m of fm.metrics) {
@@ -36557,6 +37519,21 @@ function applyFindingsInstrumentation() {
36557
37519
  setFindingsInstrumentation(true);
36558
37520
  }
36559
37521
  }
37522
+ function parseProjectProfileArg(raw) {
37523
+ if (raw === undefined || raw === true || raw === "")
37524
+ return;
37525
+ const s = String(raw).toLowerCase();
37526
+ if (s === "unknown")
37527
+ return "unknown";
37528
+ const m = /^(library|application|cli|server|plugin)\/(production|dev|sample|benchmark|test)$/.exec(s);
37529
+ if (!m) {
37530
+ console.error(colors.yellow(`Warning: invalid --project-profile "${s}" — expected one of:
37531
+ ` + ` {library,application,cli,server,plugin}/{production,dev,sample,benchmark,test}
37532
+ or "unknown". Falling back to auto-detection.`));
37533
+ return;
37534
+ }
37535
+ return s;
37536
+ }
36560
37537
  function parseCrossFileBudgetMs(raw) {
36561
37538
  if (raw === undefined || raw === true || raw === "")
36562
37539
  return;
@@ -36629,7 +37606,10 @@ Usage: cognium-dev scan <path> [options]`);
36629
37606
  excludeCwe: options["exclude-cwe"],
36630
37607
  profile: options.profile || options.p,
36631
37608
  disablePass: options["disable-pass"],
36632
- crossFileBudgetMs: parseCrossFileBudgetMs(options["cross-file-budget-ms"])
37609
+ crossFileBudgetMs: parseCrossFileBudgetMs(options["cross-file-budget-ms"]),
37610
+ projectProfile: parseProjectProfileArg(options["project-profile"]),
37611
+ noProjectProfile: options["no-project-profile"] === true,
37612
+ projectProfileExplain: options["project-profile-explain"] === true
36633
37613
  };
36634
37614
  await runScan(targetPath, scanOptions);
36635
37615
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.104.0",
3
+ "version": "3.106.0",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -65,7 +65,7 @@
65
65
  "registry": "https://registry.npmjs.org/"
66
66
  },
67
67
  "dependencies": {
68
- "circle-ir": "^3.104.0"
68
+ "circle-ir": "^3.106.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",