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.
- 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 +630 -0
- package/dist/core/circle-ir-core.cjs +2 -0
- package/dist/core/circle-ir-core.js +2 -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 +1 -1
- package/dist/languages/index.js +1 -1
- 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 +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/wasm/tree-sitter-html.wasm +0 -0
- 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
|
*/
|