circle-ir 3.16.7 → 3.17.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 (34) hide show
  1. package/dist/analysis/html/html-attribute-security-pass.d.ts +22 -0
  2. package/dist/analysis/html/html-attribute-security-pass.js +269 -0
  3. package/dist/analysis/html/html-attribute-security-pass.js.map +1 -0
  4. package/dist/analysis/html/html-extractor.d.ts +69 -0
  5. package/dist/analysis/html/html-extractor.js +169 -0
  6. package/dist/analysis/html/html-extractor.js.map +1 -0
  7. package/dist/analysis/html/html-merge.d.ts +22 -0
  8. package/dist/analysis/html/html-merge.js +164 -0
  9. package/dist/analysis/html/html-merge.js.map +1 -0
  10. package/dist/analysis/html/index.d.ts +11 -0
  11. package/dist/analysis/html/index.js +12 -0
  12. package/dist/analysis/html/index.js.map +1 -0
  13. package/dist/analyzer.js +84 -0
  14. package/dist/analyzer.js.map +1 -1
  15. package/dist/browser/circle-ir.js +630 -0
  16. package/dist/core/circle-ir-core.cjs +2 -0
  17. package/dist/core/circle-ir-core.js +2 -0
  18. package/dist/core/parser.d.ts +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/languages/index.d.ts +1 -1
  23. package/dist/languages/index.js +1 -1
  24. package/dist/languages/index.js.map +1 -1
  25. package/dist/languages/plugins/html.d.ts +33 -0
  26. package/dist/languages/plugins/html.js +84 -0
  27. package/dist/languages/plugins/html.js.map +1 -0
  28. package/dist/languages/plugins/index.d.ts +1 -0
  29. package/dist/languages/plugins/index.js +3 -0
  30. package/dist/languages/plugins/index.js.map +1 -1
  31. package/dist/languages/types.d.ts +1 -1
  32. package/dist/types/index.d.ts +1 -1
  33. package/dist/wasm/tree-sitter-html.wasm +0 -0
  34. package/package.json +3 -2
@@ -12735,6 +12735,8 @@ var ExpressionEvaluator = class {
12735
12735
  this.source = source;
12736
12736
  this.getSymbol = getSymbol;
12737
12737
  }
12738
+ source;
12739
+ getSymbol;
12738
12740
  /**
12739
12741
  * Evaluate an expression node to determine its constant value.
12740
12742
  */
@@ -17499,6 +17501,76 @@ var BashPlugin = class extends BaseLanguagePlugin {
17499
17501
  }
17500
17502
  };
17501
17503
 
17504
+ // src/languages/plugins/html.ts
17505
+ var HtmlPlugin = class extends BaseLanguagePlugin {
17506
+ id = "html";
17507
+ name = "HTML";
17508
+ extensions = [".html", ".htm", ".xhtml"];
17509
+ wasmPath = "tree-sitter-html.wasm";
17510
+ nodeTypes = {
17511
+ // HTML has no OOP constructs
17512
+ classDeclaration: [],
17513
+ interfaceDeclaration: [],
17514
+ enumDeclaration: [],
17515
+ functionDeclaration: [],
17516
+ methodDeclaration: [],
17517
+ // No expressions in HTML
17518
+ methodCall: [],
17519
+ functionCall: [],
17520
+ assignment: [],
17521
+ variableDeclaration: [],
17522
+ // No parameters
17523
+ parameter: [],
17524
+ argument: [],
17525
+ // No annotations
17526
+ annotation: [],
17527
+ decorator: [],
17528
+ // No imports
17529
+ importStatement: [],
17530
+ // No control flow
17531
+ ifStatement: [],
17532
+ forStatement: [],
17533
+ whileStatement: [],
17534
+ tryStatement: [],
17535
+ returnStatement: []
17536
+ };
17537
+ detectFramework(_context) {
17538
+ return void 0;
17539
+ }
17540
+ getBuiltinSources() {
17541
+ return [];
17542
+ }
17543
+ getBuiltinSinks() {
17544
+ return [];
17545
+ }
17546
+ getReceiverType(_node, _context) {
17547
+ return void 0;
17548
+ }
17549
+ isStringLiteral(node) {
17550
+ return node.type === "attribute_value" || node.type === "quoted_attribute_value";
17551
+ }
17552
+ getStringValue(node) {
17553
+ if (!this.isStringLiteral(node)) return void 0;
17554
+ const text = node.text;
17555
+ if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
17556
+ return text.slice(1, -1);
17557
+ }
17558
+ return text;
17559
+ }
17560
+ extractTypes(_context) {
17561
+ return [];
17562
+ }
17563
+ extractCalls(_context) {
17564
+ return [];
17565
+ }
17566
+ extractImports(_context) {
17567
+ return [];
17568
+ }
17569
+ extractPackage(_context) {
17570
+ return void 0;
17571
+ }
17572
+ };
17573
+
17502
17574
  // src/languages/plugins/index.ts
17503
17575
  function registerBuiltinPlugins() {
17504
17576
  registerLanguage(new JavaPlugin());
@@ -17506,6 +17578,7 @@ function registerBuiltinPlugins() {
17506
17578
  registerLanguage(new PythonPlugin());
17507
17579
  registerLanguage(new RustPlugin());
17508
17580
  registerLanguage(new BashPlugin());
17581
+ registerLanguage(new HtmlPlugin());
17509
17582
  }
17510
17583
 
17511
17584
  // src/utils/logger.ts
@@ -17571,6 +17644,499 @@ var logger = {
17571
17644
  }
17572
17645
  };
17573
17646
 
17647
+ // src/analysis/html/html-extractor.ts
17648
+ var EVENT_HANDLER_ATTRS = /* @__PURE__ */ new Set([
17649
+ "onclick",
17650
+ "ondblclick",
17651
+ "onmousedown",
17652
+ "onmouseup",
17653
+ "onmouseover",
17654
+ "onmousemove",
17655
+ "onmouseout",
17656
+ "onmouseenter",
17657
+ "onmouseleave",
17658
+ "onkeydown",
17659
+ "onkeyup",
17660
+ "onkeypress",
17661
+ "onfocus",
17662
+ "onblur",
17663
+ "onchange",
17664
+ "oninput",
17665
+ "onsubmit",
17666
+ "onreset",
17667
+ "onload",
17668
+ "onerror",
17669
+ "onabort",
17670
+ "onresize",
17671
+ "onscroll",
17672
+ "oncontextmenu",
17673
+ "ondrag",
17674
+ "ondrop",
17675
+ "oncopy",
17676
+ "onpaste",
17677
+ "oncut",
17678
+ "ontouchstart",
17679
+ "ontouchend",
17680
+ "ontouchmove",
17681
+ "onanimationend",
17682
+ "onanimationstart",
17683
+ "ontransitionend"
17684
+ ]);
17685
+ function extractHtmlContent(rootNode) {
17686
+ const scriptBlocks = [];
17687
+ const eventHandlers = [];
17688
+ walkNode(rootNode, scriptBlocks, eventHandlers);
17689
+ return { scriptBlocks, eventHandlers };
17690
+ }
17691
+ function walkNode(node, scriptBlocks, eventHandlers) {
17692
+ if (node.type === "script_element") {
17693
+ extractScriptBlock(node, scriptBlocks);
17694
+ }
17695
+ if (node.type === "element" || node.type === "self_closing_tag") {
17696
+ extractEventHandlers(node, eventHandlers);
17697
+ }
17698
+ for (let i2 = 0; i2 < node.childCount; i2++) {
17699
+ const child = node.child(i2);
17700
+ if (child) {
17701
+ walkNode(child, scriptBlocks, eventHandlers);
17702
+ }
17703
+ }
17704
+ }
17705
+ function extractScriptBlock(scriptNode, scriptBlocks) {
17706
+ const startTag = scriptNode.childForFieldName("start_tag") ?? findChildByType2(scriptNode, "start_tag");
17707
+ const src = getAttributeValue(startTag, "src");
17708
+ if (src) {
17709
+ scriptBlocks.push({
17710
+ code: "",
17711
+ lineOffset: scriptNode.startPosition.row + 1,
17712
+ kind: "external-src",
17713
+ src,
17714
+ scriptType: getAttributeValue(startTag, "type") ?? getAttributeValue(startTag, "lang")
17715
+ });
17716
+ return;
17717
+ }
17718
+ const rawText = findChildByType2(scriptNode, "raw_text");
17719
+ if (rawText && rawText.text.trim()) {
17720
+ scriptBlocks.push({
17721
+ code: rawText.text,
17722
+ lineOffset: rawText.startPosition.row + 1,
17723
+ kind: "inline",
17724
+ scriptType: getAttributeValue(startTag, "type") ?? getAttributeValue(startTag, "lang")
17725
+ });
17726
+ }
17727
+ }
17728
+ function extractEventHandlers(elementNode, eventHandlers) {
17729
+ const tagName = getTagName(elementNode);
17730
+ const tag = elementNode.type === "self_closing_tag" ? elementNode : findChildByType2(elementNode, "start_tag");
17731
+ if (!tag) return;
17732
+ for (let i2 = 0; i2 < tag.childCount; i2++) {
17733
+ const child = tag.child(i2);
17734
+ if (!child || child.type !== "attribute") continue;
17735
+ const nameNode = findChildByType2(child, "attribute_name");
17736
+ if (!nameNode) continue;
17737
+ const attrName = nameNode.text.toLowerCase();
17738
+ if (!EVENT_HANDLER_ATTRS.has(attrName)) continue;
17739
+ const valueNode = findChildByType2(child, "quoted_attribute_value") ?? findChildByType2(child, "attribute_value");
17740
+ if (!valueNode) continue;
17741
+ const code = stripQuotes(valueNode.text);
17742
+ if (code) {
17743
+ eventHandlers.push({
17744
+ code,
17745
+ eventName: attrName,
17746
+ line: child.startPosition.row + 1,
17747
+ element: tagName
17748
+ });
17749
+ }
17750
+ }
17751
+ }
17752
+ function getTagName(node) {
17753
+ if (node.type === "self_closing_tag") {
17754
+ const tagNameNode = findChildByType2(node, "tag_name");
17755
+ return tagNameNode?.text ?? "unknown";
17756
+ }
17757
+ const startTag = findChildByType2(node, "start_tag");
17758
+ if (startTag) {
17759
+ const tagNameNode = findChildByType2(startTag, "tag_name");
17760
+ return tagNameNode?.text ?? "unknown";
17761
+ }
17762
+ return "unknown";
17763
+ }
17764
+ function getAttributeValue(tag, name2) {
17765
+ if (!tag) return void 0;
17766
+ for (let i2 = 0; i2 < tag.childCount; i2++) {
17767
+ const child = tag.child(i2);
17768
+ if (!child || child.type !== "attribute") continue;
17769
+ const nameNode = findChildByType2(child, "attribute_name");
17770
+ if (nameNode?.text.toLowerCase() === name2) {
17771
+ const valueNode = findChildByType2(child, "quoted_attribute_value") ?? findChildByType2(child, "attribute_value");
17772
+ return valueNode ? stripQuotes(valueNode.text) : "";
17773
+ }
17774
+ }
17775
+ return void 0;
17776
+ }
17777
+ function findChildByType2(node, type) {
17778
+ for (let i2 = 0; i2 < node.childCount; i2++) {
17779
+ const child = node.child(i2);
17780
+ if (child?.type === type) return child;
17781
+ }
17782
+ return null;
17783
+ }
17784
+ function stripQuotes(text) {
17785
+ if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
17786
+ return text.slice(1, -1);
17787
+ }
17788
+ return text;
17789
+ }
17790
+
17791
+ // src/analysis/html/html-attribute-security-pass.ts
17792
+ function runHtmlAttributeSecurityChecks(rootNode, filePath) {
17793
+ const findings = [];
17794
+ walkForSecurityChecks(rootNode, filePath, findings);
17795
+ return findings;
17796
+ }
17797
+ function walkForSecurityChecks(node, filePath, findings) {
17798
+ if (node.type === "element" || node.type === "self_closing_tag" || node.type === "script_element" || node.type === "style_element") {
17799
+ checkElement(node, filePath, findings);
17800
+ }
17801
+ for (let i2 = 0; i2 < node.childCount; i2++) {
17802
+ const child = node.child(i2);
17803
+ if (child) {
17804
+ walkForSecurityChecks(child, filePath, findings);
17805
+ }
17806
+ }
17807
+ }
17808
+ function checkElement(node, filePath, findings) {
17809
+ const tagName = getTagName(node).toLowerCase();
17810
+ const tag = node.type === "self_closing_tag" ? node : findChildByType2(node, "start_tag");
17811
+ if (!tag) return;
17812
+ const line = tag.startPosition.row + 1;
17813
+ const snippet = tag.text.length > 120 ? tag.text.slice(0, 120) + "..." : tag.text;
17814
+ if (tagName === "a") {
17815
+ checkMissingNoopener(tag, filePath, line, snippet, findings);
17816
+ }
17817
+ checkJavascriptUri(tag, filePath, line, snippet, findings);
17818
+ if (tagName === "iframe") {
17819
+ checkMissingSandbox(tag, filePath, line, snippet, findings);
17820
+ }
17821
+ if (["script", "link", "img", "iframe", "video", "audio", "source", "object", "embed"].includes(tagName)) {
17822
+ checkMixedContent(tag, tagName, filePath, line, snippet, findings);
17823
+ }
17824
+ if (tagName === "script" || tagName === "link") {
17825
+ checkMissingSri(tag, tagName, filePath, line, snippet, findings);
17826
+ }
17827
+ if (tagName === "input") {
17828
+ checkAutocompleteSensitive(tag, filePath, line, snippet, findings);
17829
+ }
17830
+ checkInlineEventHandlers(tag, filePath, line, findings);
17831
+ if (tagName === "form") {
17832
+ checkFormActionJavascript(tag, filePath, line, snippet, findings);
17833
+ }
17834
+ }
17835
+ function checkMissingNoopener(tag, filePath, line, snippet, findings) {
17836
+ const target = getAttributeValue(tag, "target");
17837
+ if (target !== "_blank") return;
17838
+ const rel = getAttributeValue(tag, "rel")?.toLowerCase() ?? "";
17839
+ if (rel.includes("noopener") || rel.includes("noreferrer")) return;
17840
+ findings.push({
17841
+ id: `html-missing-noopener-${filePath}-${line}`,
17842
+ pass: "html-missing-noopener",
17843
+ category: "security",
17844
+ rule_id: "html-missing-noopener",
17845
+ cwe: "CWE-1022",
17846
+ severity: "medium",
17847
+ level: "warning",
17848
+ message: '<a target="_blank"> is missing rel="noopener" \u2014 may allow reverse tabnapping',
17849
+ file: filePath,
17850
+ line,
17851
+ snippet
17852
+ });
17853
+ }
17854
+ function checkJavascriptUri(tag, filePath, line, snippet, findings) {
17855
+ for (const attr of ["href", "src", "action"]) {
17856
+ const value = getAttributeValue(tag, attr);
17857
+ if (value && value.trim().toLowerCase().startsWith("javascript:")) {
17858
+ findings.push({
17859
+ id: `html-javascript-uri-${filePath}-${line}-${attr}`,
17860
+ pass: "html-javascript-uri",
17861
+ category: "security",
17862
+ rule_id: "html-javascript-uri",
17863
+ cwe: "CWE-79",
17864
+ severity: "high",
17865
+ level: "error",
17866
+ message: `${attr}="javascript:..." is an XSS vector \u2014 use event listeners instead`,
17867
+ file: filePath,
17868
+ line,
17869
+ snippet
17870
+ });
17871
+ }
17872
+ }
17873
+ }
17874
+ function checkMissingSandbox(tag, filePath, line, snippet, findings) {
17875
+ const sandbox = getAttributeValue(tag, "sandbox");
17876
+ if (sandbox !== void 0) return;
17877
+ findings.push({
17878
+ id: `html-missing-sandbox-${filePath}-${line}`,
17879
+ pass: "html-missing-sandbox",
17880
+ category: "security",
17881
+ rule_id: "html-missing-sandbox",
17882
+ cwe: "CWE-1021",
17883
+ severity: "medium",
17884
+ level: "warning",
17885
+ message: "<iframe> without sandbox attribute \u2014 embedded content has full privileges",
17886
+ file: filePath,
17887
+ line,
17888
+ snippet
17889
+ });
17890
+ }
17891
+ function checkMixedContent(tag, tagName, filePath, line, snippet, findings) {
17892
+ const attrName = tagName === "link" ? "href" : "src";
17893
+ const value = getAttributeValue(tag, attrName);
17894
+ if (!value || !value.startsWith("http://")) return;
17895
+ findings.push({
17896
+ id: `html-mixed-content-${filePath}-${line}`,
17897
+ pass: "html-mixed-content",
17898
+ category: "security",
17899
+ rule_id: "html-mixed-content",
17900
+ cwe: "CWE-319",
17901
+ severity: "medium",
17902
+ level: "warning",
17903
+ message: `Loading resource over HTTP (${attrName}="${truncate(value, 60)}") \u2014 use HTTPS to prevent MITM`,
17904
+ file: filePath,
17905
+ line,
17906
+ snippet
17907
+ });
17908
+ }
17909
+ function checkMissingSri(tag, tagName, filePath, line, snippet, findings) {
17910
+ const url = tagName === "script" ? getAttributeValue(tag, "src") : getAttributeValue(tag, "href");
17911
+ if (!url) return;
17912
+ if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("//")) return;
17913
+ if (tagName === "link") {
17914
+ const rel = getAttributeValue(tag, "rel")?.toLowerCase();
17915
+ if (rel !== "stylesheet") return;
17916
+ }
17917
+ const integrity = getAttributeValue(tag, "integrity");
17918
+ if (integrity) return;
17919
+ findings.push({
17920
+ id: `html-missing-sri-${filePath}-${line}`,
17921
+ pass: "html-missing-sri",
17922
+ category: "security",
17923
+ rule_id: "html-missing-sri",
17924
+ cwe: "CWE-353",
17925
+ severity: "medium",
17926
+ level: "warning",
17927
+ message: `External ${tagName === "script" ? "script" : "stylesheet"} without integrity attribute \u2014 vulnerable to CDN compromise`,
17928
+ file: filePath,
17929
+ line,
17930
+ snippet
17931
+ });
17932
+ }
17933
+ function checkAutocompleteSensitive(tag, filePath, line, snippet, findings) {
17934
+ const type = getAttributeValue(tag, "type")?.toLowerCase();
17935
+ const name2 = getAttributeValue(tag, "name")?.toLowerCase() ?? "";
17936
+ const isSensitive = type === "password" || /\b(ssn|social.?security|credit.?card|card.?number|cvv|cvc|ccv)\b/.test(name2);
17937
+ if (!isSensitive) return;
17938
+ const autocomplete = getAttributeValue(tag, "autocomplete")?.toLowerCase();
17939
+ if (autocomplete === "off" || autocomplete === "new-password") return;
17940
+ findings.push({
17941
+ id: `html-autocomplete-sensitive-${filePath}-${line}`,
17942
+ pass: "html-autocomplete-sensitive",
17943
+ category: "security",
17944
+ rule_id: "html-autocomplete-sensitive",
17945
+ cwe: "CWE-525",
17946
+ severity: "low",
17947
+ level: "note",
17948
+ message: 'Sensitive input field without autocomplete="off" \u2014 browser may cache sensitive data',
17949
+ file: filePath,
17950
+ line,
17951
+ snippet
17952
+ });
17953
+ }
17954
+ function checkInlineEventHandlers(tag, filePath, line, findings) {
17955
+ for (let i2 = 0; i2 < tag.childCount; i2++) {
17956
+ const child = tag.child(i2);
17957
+ if (!child || child.type !== "attribute") continue;
17958
+ const nameNode = findChildByType2(child, "attribute_name");
17959
+ if (!nameNode) continue;
17960
+ const attrName = nameNode.text.toLowerCase();
17961
+ if (attrName.startsWith("on") && attrName.length > 2) {
17962
+ const attrLine = child.startPosition.row + 1;
17963
+ findings.push({
17964
+ id: `html-inline-event-handler-${filePath}-${attrLine}-${attrName}`,
17965
+ pass: "html-inline-event-handler",
17966
+ category: "security",
17967
+ rule_id: "html-inline-event-handler",
17968
+ cwe: "CWE-79",
17969
+ severity: "low",
17970
+ level: "note",
17971
+ message: `Inline ${attrName} handler \u2014 incompatible with strict Content Security Policy; use addEventListener() instead`,
17972
+ file: filePath,
17973
+ line: attrLine,
17974
+ snippet: child.text
17975
+ });
17976
+ }
17977
+ }
17978
+ }
17979
+ function checkFormActionJavascript(tag, filePath, line, snippet, findings) {
17980
+ const action = getAttributeValue(tag, "action");
17981
+ if (!action || !action.trim().toLowerCase().startsWith("javascript:")) return;
17982
+ findings.push({
17983
+ id: `html-form-action-javascript-${filePath}-${line}`,
17984
+ pass: "html-form-action-javascript",
17985
+ category: "security",
17986
+ rule_id: "html-form-action-javascript",
17987
+ cwe: "CWE-79",
17988
+ severity: "high",
17989
+ level: "error",
17990
+ message: '<form action="javascript:..."> is an XSS vector \u2014 use proper form submission',
17991
+ file: filePath,
17992
+ line,
17993
+ snippet
17994
+ });
17995
+ }
17996
+ function truncate(s, maxLen) {
17997
+ return s.length > maxLen ? s.slice(0, maxLen) + "..." : s;
17998
+ }
17999
+
18000
+ // src/analysis/html/html-merge.ts
18001
+ function mergeHtmlResults(htmlMeta, scriptResults, attributeFindings) {
18002
+ const allTypes = [];
18003
+ const allCalls = [];
18004
+ const allCfgBlocks = [];
18005
+ const allCfgEdges = [];
18006
+ const allDfgDefs = [];
18007
+ const allDfgUses = [];
18008
+ const allSources = [];
18009
+ const allSinks = [];
18010
+ const allSanitizers = [];
18011
+ const allImports = [];
18012
+ const allExports = [];
18013
+ const allFindings = [];
18014
+ let cfgBlockIdOffset = 0;
18015
+ let dfgDefIdOffset = 0;
18016
+ let dfgUseIdOffset = 0;
18017
+ for (const { ir, lineOffset } of scriptResults) {
18018
+ const lineShift = lineOffset - 1;
18019
+ const htmlFile = htmlMeta.file;
18020
+ for (const type of ir.types) {
18021
+ allTypes.push({
18022
+ ...type,
18023
+ start_line: type.start_line + lineShift,
18024
+ end_line: type.end_line + lineShift,
18025
+ methods: type.methods.map((m) => ({
18026
+ ...m,
18027
+ start_line: m.start_line + lineShift,
18028
+ end_line: m.end_line + lineShift
18029
+ })),
18030
+ fields: [...type.fields]
18031
+ });
18032
+ }
18033
+ for (const call of ir.calls) {
18034
+ allCalls.push({
18035
+ ...call,
18036
+ location: {
18037
+ ...call.location,
18038
+ line: call.location.line + lineShift
18039
+ }
18040
+ });
18041
+ }
18042
+ const maxBlockId = ir.cfg.blocks.reduce((max, b) => Math.max(max, b.id), 0);
18043
+ for (const block of ir.cfg.blocks) {
18044
+ allCfgBlocks.push({
18045
+ ...block,
18046
+ id: block.id + cfgBlockIdOffset,
18047
+ start_line: block.start_line + lineShift,
18048
+ end_line: block.end_line + lineShift
18049
+ });
18050
+ }
18051
+ for (const edge of ir.cfg.edges) {
18052
+ allCfgEdges.push({
18053
+ ...edge,
18054
+ from: edge.from + cfgBlockIdOffset,
18055
+ to: edge.to + cfgBlockIdOffset
18056
+ });
18057
+ }
18058
+ cfgBlockIdOffset += maxBlockId + 1;
18059
+ const maxDefId = ir.dfg.defs.reduce((max, d) => Math.max(max, d.id), 0);
18060
+ const maxUseId = ir.dfg.uses.reduce((max, u) => Math.max(max, u.id), 0);
18061
+ for (const def of ir.dfg.defs) {
18062
+ allDfgDefs.push({
18063
+ ...def,
18064
+ id: def.id + dfgDefIdOffset,
18065
+ line: def.line + lineShift
18066
+ });
18067
+ }
18068
+ for (const use of ir.dfg.uses) {
18069
+ allDfgUses.push({
18070
+ ...use,
18071
+ id: use.id + dfgUseIdOffset,
18072
+ def_id: use.def_id !== null ? use.def_id + dfgDefIdOffset : null,
18073
+ line: use.line + lineShift
18074
+ });
18075
+ }
18076
+ dfgDefIdOffset += maxDefId + 1;
18077
+ dfgUseIdOffset += maxUseId + 1;
18078
+ for (const source of ir.taint.sources) {
18079
+ allSources.push({
18080
+ ...source,
18081
+ line: source.line + lineShift
18082
+ });
18083
+ }
18084
+ for (const sink of ir.taint.sinks) {
18085
+ allSinks.push({
18086
+ ...sink,
18087
+ line: sink.line + lineShift
18088
+ });
18089
+ }
18090
+ for (const sanitizer of ir.taint.sanitizers ?? []) {
18091
+ allSanitizers.push({
18092
+ ...sanitizer,
18093
+ line: sanitizer.line + lineShift
18094
+ });
18095
+ }
18096
+ for (const imp of ir.imports) {
18097
+ allImports.push({
18098
+ ...imp,
18099
+ line_number: imp.line_number !== null ? imp.line_number + lineShift : null
18100
+ });
18101
+ }
18102
+ allExports.push(...ir.exports);
18103
+ for (const finding of ir.findings ?? []) {
18104
+ allFindings.push({
18105
+ ...finding,
18106
+ file: htmlFile,
18107
+ line: finding.line + lineShift
18108
+ });
18109
+ }
18110
+ }
18111
+ allFindings.push(...attributeFindings);
18112
+ const taint = {
18113
+ sources: allSources,
18114
+ sinks: allSinks,
18115
+ sanitizers: allSanitizers.length > 0 ? allSanitizers : void 0
18116
+ };
18117
+ const cfg = {
18118
+ blocks: allCfgBlocks,
18119
+ edges: allCfgEdges
18120
+ };
18121
+ const dfg = {
18122
+ defs: allDfgDefs,
18123
+ uses: allDfgUses
18124
+ };
18125
+ return {
18126
+ meta: htmlMeta,
18127
+ types: allTypes,
18128
+ calls: allCalls,
18129
+ cfg,
18130
+ dfg,
18131
+ taint,
18132
+ imports: allImports,
18133
+ exports: allExports,
18134
+ unresolved: [],
18135
+ enriched: {},
18136
+ findings: allFindings.length > 0 ? allFindings : void 0
18137
+ };
18138
+ }
18139
+
17574
18140
  // src/analysis/passes/taint-matcher-pass.ts
17575
18141
  var TaintMatcherPass = class {
17576
18142
  name = "taint-matcher";
@@ -17637,6 +18203,7 @@ var ConstantPropagationPass = class {
17637
18203
  constructor(tree) {
17638
18204
  this.tree = tree;
17639
18205
  }
18206
+ tree;
17640
18207
  name = "constant-propagation";
17641
18208
  category = "security";
17642
18209
  run(ctx) {
@@ -22760,6 +23327,16 @@ function getNodeTypesForLanguage(language) {
22760
23327
  "c_style_for_statement",
22761
23328
  "while_statement"
22762
23329
  ]);
23330
+ case "html":
23331
+ return /* @__PURE__ */ new Set([
23332
+ "element",
23333
+ "script_element",
23334
+ "style_element",
23335
+ "attribute",
23336
+ "start_tag",
23337
+ "self_closing_tag",
23338
+ "text"
23339
+ ]);
22763
23340
  default:
22764
23341
  return /* @__PURE__ */ new Set([
22765
23342
  "method_invocation",
@@ -22778,6 +23355,9 @@ async function analyze(code, filePath, language, options = {}) {
22778
23355
  if (!initialized) {
22779
23356
  await initAnalyzer(options);
22780
23357
  }
23358
+ if (language === "html") {
23359
+ return analyzeHtmlFile(code, filePath, options);
23360
+ }
22781
23361
  logger.debug("Analyzing file", { filePath, language, codeLength: code.length });
22782
23362
  const tree = await parse(code, language);
22783
23363
  logger.trace("Parsed AST", { rootNodeType: tree.rootNode.type });
@@ -22884,6 +23464,56 @@ async function analyze(code, filePath, language, options = {}) {
22884
23464
  metrics: { file: filePath, metrics: metricValues }
22885
23465
  };
22886
23466
  }
23467
+ async function analyzeHtmlFile(code, filePath, options) {
23468
+ logger.debug("Analyzing HTML file", { filePath, codeLength: code.length });
23469
+ const tree = await parse(code, "html");
23470
+ const meta = extractMeta(code, tree, filePath, "html");
23471
+ const { scriptBlocks, eventHandlers } = extractHtmlContent(tree.rootNode);
23472
+ logger.debug("HTML extraction", {
23473
+ filePath,
23474
+ inlineScripts: scriptBlocks.filter((b) => b.kind === "inline").length,
23475
+ externalScripts: scriptBlocks.filter((b) => b.kind === "external-src").length,
23476
+ eventHandlers: eventHandlers.length
23477
+ });
23478
+ const scriptResults = [];
23479
+ for (const block of scriptBlocks) {
23480
+ if (block.kind !== "inline" || !block.code.trim()) continue;
23481
+ const scriptLang = block.scriptType === "ts" || block.scriptType === "typescript" || block.scriptType === "text/typescript" ? "typescript" : "javascript";
23482
+ try {
23483
+ const ir = await analyze(block.code, filePath, scriptLang, options);
23484
+ scriptResults.push({ ir, lineOffset: block.lineOffset });
23485
+ } catch (e) {
23486
+ logger.warn("Failed to analyze script block", {
23487
+ filePath,
23488
+ lineOffset: block.lineOffset,
23489
+ error: e instanceof Error ? e.message : String(e)
23490
+ });
23491
+ }
23492
+ }
23493
+ for (const handler of eventHandlers) {
23494
+ const wrappedCode = `function __${handler.eventName}_handler() { ${handler.code} }`;
23495
+ try {
23496
+ const ir = await analyze(wrappedCode, filePath, "javascript", options);
23497
+ scriptResults.push({ ir, lineOffset: handler.line });
23498
+ } catch (e) {
23499
+ logger.warn("Failed to analyze event handler", {
23500
+ filePath,
23501
+ eventName: handler.eventName,
23502
+ line: handler.line,
23503
+ error: e instanceof Error ? e.message : String(e)
23504
+ });
23505
+ }
23506
+ }
23507
+ const attributeFindings = runHtmlAttributeSecurityChecks(tree.rootNode, filePath);
23508
+ const result = mergeHtmlResults(meta, scriptResults, attributeFindings);
23509
+ logger.debug("HTML analysis complete", {
23510
+ filePath,
23511
+ scriptBlocks: scriptResults.length,
23512
+ attributeFindings: attributeFindings.length,
23513
+ totalFindings: result.findings?.length ?? 0
23514
+ });
23515
+ return result;
23516
+ }
22887
23517
  async function analyzeForAPI(code, filePath, language, options = {}) {
22888
23518
  const startTime = performance.now();
22889
23519
  if (!initialized) {
@@ -11528,6 +11528,8 @@ var ExpressionEvaluator = class {
11528
11528
  this.source = source;
11529
11529
  this.getSymbol = getSymbol;
11530
11530
  }
11531
+ source;
11532
+ getSymbol;
11531
11533
  /**
11532
11534
  * Evaluate an expression node to determine its constant value.
11533
11535
  */
@@ -11463,6 +11463,8 @@ var ExpressionEvaluator = class {
11463
11463
  this.source = source;
11464
11464
  this.getSymbol = getSymbol;
11465
11465
  }
11466
+ source;
11467
+ getSymbol;
11466
11468
  /**
11467
11469
  * Evaluate an expression node to determine its constant value.
11468
11470
  */