cognium-dev 3.138.0 → 3.139.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 +151 -1
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -38092,6 +38092,154 @@ class UnrestrictedFileUploadPass {
38092
38092
  }
38093
38093
  }
38094
38094
 
38095
+ // ../circle-ir/dist/analysis/passes/missing-sanitizer-gate-pass.js
38096
+ var HTML_OUTPUT_SINKS = new Set([
38097
+ "addAttribute",
38098
+ "setAttribute",
38099
+ "write",
38100
+ "writeAttribute",
38101
+ "writeRaw",
38102
+ "writeStartElement",
38103
+ "writeEndElement",
38104
+ "writeCharacters",
38105
+ "printXML",
38106
+ "printXMLElement",
38107
+ "printXMLStartElement",
38108
+ "printXMLEndElement",
38109
+ "printRaw",
38110
+ "print",
38111
+ "println",
38112
+ "printf",
38113
+ "append",
38114
+ "format"
38115
+ ]);
38116
+ var SANITIZER_METHODS2 = new Set([
38117
+ "isAttributeAllowed",
38118
+ "isElementAllowed",
38119
+ "isSafe",
38120
+ "sanitize",
38121
+ "clean",
38122
+ "escape",
38123
+ "encode",
38124
+ "forHtml",
38125
+ "forHtmlAttribute",
38126
+ "forHtmlContent",
38127
+ "encodeForHTML",
38128
+ "encodeForHTMLAttribute",
38129
+ "escapeHtml",
38130
+ "escapeHtml4",
38131
+ "escapeXml",
38132
+ "htmlEscape",
38133
+ "xmlEscape"
38134
+ ]);
38135
+ var SANITIZER_PREFIX_RE = /^(?:clean|sanitize|escape|encode)[A-Z]/;
38136
+ function isSanitizerCall(methodName) {
38137
+ return SANITIZER_METHODS2.has(methodName) || SANITIZER_PREFIX_RE.test(methodName);
38138
+ }
38139
+ var STRING_PARAM_NAME_PATTERN = /attr|attribute|html|xml|body|content/i;
38140
+
38141
+ class MissingSanitizerGatePass {
38142
+ name = "missing-sanitizer-gate";
38143
+ category = "security";
38144
+ run(ctx) {
38145
+ const { graph, language } = ctx;
38146
+ if (language !== "java")
38147
+ return { findings: 0 };
38148
+ const { cfg, calls, types } = graph.ir;
38149
+ if (cfg.blocks.length === 0 || cfg.edges.length === 0)
38150
+ return { findings: 0 };
38151
+ if (calls.length === 0)
38152
+ return { findings: 0 };
38153
+ const file = graph.ir.meta.file;
38154
+ const sinkCalls = [];
38155
+ const sanitizerLines = [];
38156
+ for (const call of calls) {
38157
+ if (HTML_OUTPUT_SINKS.has(call.method_name)) {
38158
+ sinkCalls.push({ line: call.location.line, method: call.method_name });
38159
+ }
38160
+ if (isSanitizerCall(call.method_name)) {
38161
+ sanitizerLines.push(call.location.line);
38162
+ }
38163
+ }
38164
+ if (sinkCalls.length === 0)
38165
+ return { findings: 0 };
38166
+ const blockContainingLine = (line) => cfg.blocks.find((b) => b.start_line <= line && line <= b.end_line) ?? null;
38167
+ const domByMethodKey = new Map;
38168
+ const getDomForMethod = (methodKey, method) => {
38169
+ if (domByMethodKey.has(methodKey))
38170
+ return domByMethodKey.get(methodKey) ?? null;
38171
+ const entry = cfg.blocks.find((b) => b.type === "entry" && b.start_line >= method.start_line && b.end_line <= method.end_line) ?? cfg.blocks.find((b) => b.start_line >= method.start_line && b.end_line <= method.end_line);
38172
+ const dom = entry ? new DominatorGraph2(cfg, entry.id) : null;
38173
+ domByMethodKey.set(methodKey, dom);
38174
+ return dom;
38175
+ };
38176
+ const reportedMethods = new Set;
38177
+ let count = 0;
38178
+ for (const sink of sinkCalls) {
38179
+ const sinkBlock = blockContainingLine(sink.line);
38180
+ if (!sinkBlock)
38181
+ continue;
38182
+ const methodInfo = graph.methodAtLine(sink.line);
38183
+ if (!methodInfo)
38184
+ continue;
38185
+ const methodKey = `${methodInfo.type.name}::${methodInfo.method.name}`;
38186
+ if (reportedMethods.has(methodKey))
38187
+ continue;
38188
+ const tier = classifyEntryPointTier(methodInfo.method, methodInfo.type, {
38189
+ types,
38190
+ language
38191
+ });
38192
+ if (tier === "TIER_1_ENTRY_POINT")
38193
+ continue;
38194
+ if (!methodAcceptsHtmlShapedParam(methodInfo.method))
38195
+ continue;
38196
+ const dom = getDomForMethod(methodKey, methodInfo.method);
38197
+ if (!dom)
38198
+ continue;
38199
+ const { start_line, end_line } = methodInfo.method;
38200
+ const sanitizersInMethod = sanitizerLines.filter((l) => l >= start_line && l <= end_line);
38201
+ const dominated = sanitizersInMethod.some((sanLine) => {
38202
+ const sanBlock = blockContainingLine(sanLine);
38203
+ return sanBlock !== null && dom.dominates(sanBlock.id, sinkBlock.id);
38204
+ });
38205
+ if (!dominated) {
38206
+ reportedMethods.add(methodKey);
38207
+ count++;
38208
+ ctx.addFinding({
38209
+ id: `missing-sanitizer-gate-${file}-${sink.line}`,
38210
+ pass: this.name,
38211
+ category: this.category,
38212
+ rule_id: this.name,
38213
+ cwe: "CWE-79",
38214
+ severity: "medium",
38215
+ level: "note",
38216
+ confidence: "medium",
38217
+ message: `HTML output sink \`${sink.method}()\` at line ${sink.line} is not ` + `dominated by any sanitizer call in method \`${methodInfo.method.name}\`. ` + `Caller-supplied attribute/HTML values may reach the sink unescaped.`,
38218
+ file,
38219
+ line: sink.line,
38220
+ fix: `Gate the sink on all paths with an allow-list check ` + `(e.g. \`isAttributeAllowed(...)\`) or wrap the argument in an HTML/XML ` + `escaper (\`escapeHtml\`, \`encodeForHTMLAttribute\`, \`forHtmlAttribute\`).`,
38221
+ evidence: { sink: sink.method, method: methodInfo.method.name }
38222
+ });
38223
+ }
38224
+ }
38225
+ return { findings: count };
38226
+ }
38227
+ }
38228
+ function methodAcceptsHtmlShapedParam(method) {
38229
+ for (const p of method.parameters) {
38230
+ if (!p.type)
38231
+ continue;
38232
+ const type = p.type.replace(/\s+/g, "");
38233
+ if (type === "Attributes")
38234
+ return true;
38235
+ if (type.startsWith("Map<String,String"))
38236
+ return true;
38237
+ if (type === "String" && STRING_PARAM_NAME_PATTERN.test(p.name))
38238
+ return true;
38239
+ }
38240
+ return false;
38241
+ }
38242
+
38095
38243
  // ../circle-ir/dist/analysis/passes/plaintext-password-storage-pass.js
38096
38244
  function isWriteStorageCall(call, language) {
38097
38245
  const method = call.method_name ?? "";
@@ -40678,6 +40826,8 @@ async function analyze(code, filePath, language, options = {}) {
40678
40826
  pipeline.add(new InfoDisclosureStacktracePass);
40679
40827
  if (!disabledPasses.has("unrestricted-file-upload"))
40680
40828
  pipeline.add(new UnrestrictedFileUploadPass);
40829
+ if (!disabledPasses.has("missing-sanitizer-gate"))
40830
+ pipeline.add(new MissingSanitizerGatePass);
40681
40831
  const { results, findings } = pipeline.run(graph, code, language, config);
40682
40832
  const sinkFilter = results.get("sink-filter");
40683
40833
  const interProc = results.get("interprocedural");
@@ -41491,7 +41641,7 @@ var colors = {
41491
41641
  };
41492
41642
 
41493
41643
  // src/version.ts
41494
- var version = "3.138.0";
41644
+ var version = "3.139.0";
41495
41645
 
41496
41646
  // src/formatters.ts
41497
41647
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.138.0",
3
+ "version": "3.139.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",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@cognium/project-profile-detect": "^1.1.0",
69
- "circle-ir": "^3.138.0"
69
+ "circle-ir": "^3.139.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^25.5.0",