circle-ir 3.16.8 → 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.
@@ -17501,6 +17501,76 @@ var BashPlugin = class extends BaseLanguagePlugin {
17501
17501
  }
17502
17502
  };
17503
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
+
17504
17574
  // src/languages/plugins/index.ts
17505
17575
  function registerBuiltinPlugins() {
17506
17576
  registerLanguage(new JavaPlugin());
@@ -17508,6 +17578,7 @@ function registerBuiltinPlugins() {
17508
17578
  registerLanguage(new PythonPlugin());
17509
17579
  registerLanguage(new RustPlugin());
17510
17580
  registerLanguage(new BashPlugin());
17581
+ registerLanguage(new HtmlPlugin());
17511
17582
  }
17512
17583
 
17513
17584
  // src/utils/logger.ts
@@ -17573,6 +17644,499 @@ var logger = {
17573
17644
  }
17574
17645
  };
17575
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
+
17576
18140
  // src/analysis/passes/taint-matcher-pass.ts
17577
18141
  var TaintMatcherPass = class {
17578
18142
  name = "taint-matcher";
@@ -22763,6 +23327,16 @@ function getNodeTypesForLanguage(language) {
22763
23327
  "c_style_for_statement",
22764
23328
  "while_statement"
22765
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
+ ]);
22766
23340
  default:
22767
23341
  return /* @__PURE__ */ new Set([
22768
23342
  "method_invocation",
@@ -22781,6 +23355,9 @@ async function analyze(code, filePath, language, options = {}) {
22781
23355
  if (!initialized) {
22782
23356
  await initAnalyzer(options);
22783
23357
  }
23358
+ if (language === "html") {
23359
+ return analyzeHtmlFile(code, filePath, options);
23360
+ }
22784
23361
  logger.debug("Analyzing file", { filePath, language, codeLength: code.length });
22785
23362
  const tree = await parse(code, language);
22786
23363
  logger.trace("Parsed AST", { rootNodeType: tree.rootNode.type });
@@ -22887,6 +23464,56 @@ async function analyze(code, filePath, language, options = {}) {
22887
23464
  metrics: { file: filePath, metrics: metricValues }
22888
23465
  };
22889
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
+ }
22890
23517
  async function analyzeForAPI(code, filePath, language, options = {}) {
22891
23518
  const startTime = performance.now();
22892
23519
  if (!initialized) {
@@ -7,7 +7,7 @@ import { Parser, Language, Tree, Node } from 'web-tree-sitter';
7
7
  export { Language, Tree };
8
8
  export type { Node };
9
9
  export type SyntaxNode = Node;
10
- export type SupportedLanguage = 'java' | 'c' | 'cpp' | 'javascript' | 'typescript' | 'python' | 'rust' | 'bash';
10
+ export type SupportedLanguage = 'java' | 'c' | 'cpp' | 'javascript' | 'typescript' | 'python' | 'rust' | 'bash' | 'html';
11
11
  interface ParserOptions {
12
12
  /**
13
13
  * Custom path/URL to the tree-sitter.wasm file.
package/dist/index.d.ts CHANGED
@@ -17,6 +17,6 @@ export { getRuleInfo, RULE_DEFINITIONS, type RuleInfo, } from './analysis/rules.
17
17
  export { DominatorGraph } from './graph/dominator-graph.js';
18
18
  export { ExceptionFlowGraph, type TryCatchInfo } from './graph/exception-flow-graph.js';
19
19
  export { TypeHierarchyResolver, createWithJdkTypes, SymbolTable, buildSymbolTable, CrossFileResolver, buildCrossFileResolver, } from './resolution/index.js';
20
- export { getLanguageRegistry, registerLanguage, getLanguagePlugin, getLanguageForFile, detectLanguage, isLanguageSupported, registerBuiltinPlugins, JavaPlugin, JavaScriptPlugin, PythonPlugin, RustPlugin, BaseLanguagePlugin, } from './languages/index.js';
20
+ export { getLanguageRegistry, registerLanguage, getLanguagePlugin, getLanguageForFile, detectLanguage, isLanguageSupported, registerBuiltinPlugins, JavaPlugin, JavaScriptPlugin, PythonPlugin, RustPlugin, HtmlPlugin, BaseLanguagePlugin, } from './languages/index.js';
21
21
  export type { LanguagePlugin, LanguageRegistry, LanguageNodeTypes, ExtractionContext, FrameworkInfo, TaintSourcePattern, TaintSinkPattern, } from './languages/index.js';
22
22
  export { logger, setLogger, configureLogger, setLogLevel, getLogLevel, type LogLevel, type LoggerConfig, type LoggerInstance, } from './utils/logger.js';
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export { ExceptionFlowGraph } from './graph/exception-flow-graph.js';
19
19
  // Resolution utilities
20
20
  export { TypeHierarchyResolver, createWithJdkTypes, SymbolTable, buildSymbolTable, CrossFileResolver, buildCrossFileResolver, } from './resolution/index.js';
21
21
  // Language plugins
22
- export { getLanguageRegistry, registerLanguage, getLanguagePlugin, getLanguageForFile, detectLanguage, isLanguageSupported, registerBuiltinPlugins, JavaPlugin, JavaScriptPlugin, PythonPlugin, RustPlugin, BaseLanguagePlugin, } from './languages/index.js';
22
+ export { getLanguageRegistry, registerLanguage, getLanguagePlugin, getLanguageForFile, detectLanguage, isLanguageSupported, registerBuiltinPlugins, JavaPlugin, JavaScriptPlugin, PythonPlugin, RustPlugin, HtmlPlugin, BaseLanguagePlugin, } from './languages/index.js';
23
23
  // Logger (dependency injection)
24
24
  export { logger, setLogger, configureLogger, setLogLevel, getLogLevel, } from './utils/logger.js';
25
25
  //# sourceMappingURL=index.js.map