circle-ir 3.16.8 → 3.17.1
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.
- package/README.md +7 -1
- package/dist/analysis/html/html-attribute-security-pass.d.ts +22 -0
- package/dist/analysis/html/html-attribute-security-pass.js +269 -0
- package/dist/analysis/html/html-attribute-security-pass.js.map +1 -0
- package/dist/analysis/html/html-extractor.d.ts +69 -0
- package/dist/analysis/html/html-extractor.js +169 -0
- package/dist/analysis/html/html-extractor.js.map +1 -0
- package/dist/analysis/html/html-merge.d.ts +22 -0
- package/dist/analysis/html/html-merge.js +164 -0
- package/dist/analysis/html/html-merge.js.map +1 -0
- package/dist/analysis/html/index.d.ts +11 -0
- package/dist/analysis/html/index.js +12 -0
- package/dist/analysis/html/index.js.map +1 -0
- package/dist/analyzer.js +84 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +627 -0
- package/dist/core/parser.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/languages/index.d.ts +2 -2
- package/dist/languages/index.js +2 -2
- package/dist/languages/index.js.map +1 -1
- package/dist/languages/plugins/html.d.ts +33 -0
- package/dist/languages/plugins/html.js +84 -0
- package/dist/languages/plugins/html.js.map +1 -0
- package/dist/languages/plugins/index.d.ts +1 -0
- package/dist/languages/plugins/index.js +3 -0
- package/dist/languages/plugins/index.js.map +1 -1
- package/dist/languages/types.d.ts +2 -2
- package/dist/languages/types.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/wasm/tree-sitter-html.wasm +0 -0
- package/docs/SPEC.md +1 -1
- package/package.json +2 -1
|
@@ -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) {
|
package/dist/core/parser.d.ts
CHANGED
|
@@ -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
|