pi-lens 3.8.21 → 3.8.22
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/CHANGELOG.md +8 -0
- package/README.md +2 -0
- package/clients/dispatch/runners/lsp.ts +58 -3
- package/clients/dispatch/runners/tree-sitter.ts +467 -0
- package/clients/lsp/client.ts +229 -3
- package/clients/lsp/index.ts +111 -1
- package/clients/pipeline.ts +2 -2
- package/clients/runtime-session.ts +43 -5
- package/clients/tree-sitter-client.ts +162 -0
- package/clients/tree-sitter-logger.ts +47 -0
- package/clients/tree-sitter-query-loader.ts +13 -2
- package/package.json +3 -1
- package/rules/rule-catalog.json +64 -0
- package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
- package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
- package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
- package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
- package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
- package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
- package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
- package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
- package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
- package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
- package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
- package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
- package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
- package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
- package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
- package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
- package/scripts/validate-rule-catalog.mjs +227 -0
- package/skills/lsp-navigation/SKILL.md +15 -3
- package/tools/lsp-navigation.js +259 -28
- package/tools/lsp-navigation.ts +294 -29
|
@@ -892,6 +892,26 @@ export class TreeSitterClient {
|
|
|
892
892
|
if (!secretPatterns.some((p) => p.test(varName))) continue;
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
+
// Go: only keep bare-return-call matches when enclosing function returns error.
|
|
896
|
+
if (postFilter === "returns_error") {
|
|
897
|
+
const first = Object.values(captures)[0];
|
|
898
|
+
if (!first) continue;
|
|
899
|
+
const funcNode = this.navigator.findParent(first, [
|
|
900
|
+
"function_declaration",
|
|
901
|
+
"method_declaration",
|
|
902
|
+
]);
|
|
903
|
+
if (!funcNode) continue;
|
|
904
|
+
|
|
905
|
+
const fnText = String(funcNode.text ?? "");
|
|
906
|
+
const signature = fnText.split("{", 1)[0]?.trim() ?? "";
|
|
907
|
+
const returnPartMatch = signature.match(
|
|
908
|
+
/func\s*(?:\([^)]*\)\s*)?[A-Za-z_]\w*\s*\([^)]*\)\s*(.*)$/s,
|
|
909
|
+
);
|
|
910
|
+
const returnPart = returnPartMatch?.[1]?.trim() ?? "";
|
|
911
|
+
const returnsError = returnPart.length > 0 && /\berror\b/.test(returnPart);
|
|
912
|
+
if (!returnsError) continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
895
915
|
// Python: except body that only contains pass (effectively empty)
|
|
896
916
|
if (postFilter === "python_empty_except") {
|
|
897
917
|
const bodyNode = captures.BODY;
|
|
@@ -921,6 +941,148 @@ export class TreeSitterClient {
|
|
|
921
941
|
}
|
|
922
942
|
}
|
|
923
943
|
|
|
944
|
+
// TS security/concurrency: strict sink filtering (query predicates are not reliable in this runtime)
|
|
945
|
+
if (postFilter === "ts_command_injection_sink") {
|
|
946
|
+
const mod = captures.MOD?.text ?? "";
|
|
947
|
+
const fn = captures.FN?.text ?? "";
|
|
948
|
+
if (mod !== "child_process") continue;
|
|
949
|
+
if (!/^(exec|execSync)$/.test(fn)) continue;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (postFilter === "ts_ssrf_sink") {
|
|
953
|
+
const fn = captures.FN?.text ?? "";
|
|
954
|
+
const obj = captures.OBJ?.text ?? "";
|
|
955
|
+
const allowedFns = new Set([
|
|
956
|
+
"fetch",
|
|
957
|
+
"request",
|
|
958
|
+
"get",
|
|
959
|
+
"post",
|
|
960
|
+
"put",
|
|
961
|
+
"patch",
|
|
962
|
+
"delete",
|
|
963
|
+
]);
|
|
964
|
+
if (!allowedFns.has(fn)) continue;
|
|
965
|
+
|
|
966
|
+
if (!obj) {
|
|
967
|
+
if (fn !== "fetch") continue;
|
|
968
|
+
} else {
|
|
969
|
+
const allowedObjs = new Set([
|
|
970
|
+
"axios",
|
|
971
|
+
"http",
|
|
972
|
+
"https",
|
|
973
|
+
"got",
|
|
974
|
+
"request",
|
|
975
|
+
"superagent",
|
|
976
|
+
"undici",
|
|
977
|
+
]);
|
|
978
|
+
if (!allowedObjs.has(obj)) continue;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (postFilter === "ts_weak_hash_algorithm") {
|
|
983
|
+
const fn = captures.FN?.text ?? "";
|
|
984
|
+
const alg = captures.ALG?.text ?? "";
|
|
985
|
+
if (fn !== "createHash") continue;
|
|
986
|
+
if (!/^(md5|sha1)$/i.test(alg)) continue;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (postFilter === "ts_insecure_random_source") {
|
|
990
|
+
const obj = captures.OBJ?.text ?? "";
|
|
991
|
+
const fn = captures.FN?.text ?? "";
|
|
992
|
+
if (obj !== "Math" || fn !== "random") continue;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (postFilter === "ts_detached_async_call") {
|
|
996
|
+
const fn = captures.FN?.text ?? "";
|
|
997
|
+
if (!/(Async$|fetch$|request$)/.test(fn)) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (postFilter === "py_command_injection_sink") {
|
|
1003
|
+
const mod = captures.MOD?.text ?? "";
|
|
1004
|
+
const fn = captures.FN?.text ?? "";
|
|
1005
|
+
const kw = captures.KW?.text ?? "";
|
|
1006
|
+
const isOs = mod === "os" && /^(system|popen)$/.test(fn);
|
|
1007
|
+
const isSubprocess =
|
|
1008
|
+
mod === "subprocess" &&
|
|
1009
|
+
/^(run|Popen|call|check_output|check_call)$/.test(fn) &&
|
|
1010
|
+
kw === "shell";
|
|
1011
|
+
if (!isOs && !isSubprocess) continue;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (postFilter === "go_command_injection_sink") {
|
|
1015
|
+
const pkg = captures.PKG?.text ?? "";
|
|
1016
|
+
const fn = captures.FN?.text ?? "";
|
|
1017
|
+
const shell = captures.SHELL?.text ?? "";
|
|
1018
|
+
const flag = captures.FLAG?.text ?? "";
|
|
1019
|
+
if (pkg !== "exec") continue;
|
|
1020
|
+
if (!/^(Command|CommandContext)$/.test(fn)) continue;
|
|
1021
|
+
if (!/^"(sh|bash|zsh|cmd|powershell|pwsh)"$/.test(shell)) continue;
|
|
1022
|
+
if (!/^"(-c|\/c)"$/.test(flag)) continue;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (postFilter === "ruby_command_injection_sink") {
|
|
1026
|
+
const fn = captures.FN?.text ?? "";
|
|
1027
|
+
if (!/^(system|exec|spawn|popen|capture3|capture2|capture2e)$/.test(fn)) {
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (postFilter === "py_ssrf_sink") {
|
|
1033
|
+
const mod = captures.MOD?.text ?? "";
|
|
1034
|
+
const fn = captures.FN?.text ?? "";
|
|
1035
|
+
if (mod !== "requests") continue;
|
|
1036
|
+
if (!/^(get|post|put|patch|delete|request|head|options)$/.test(fn))
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (postFilter === "py_path_traversal_sink") {
|
|
1041
|
+
const fn = captures.FN?.text ?? "";
|
|
1042
|
+
if (
|
|
1043
|
+
!/^(open|read_text|read_bytes|write_text|write_bytes|remove|unlink|rmdir)$/.test(
|
|
1044
|
+
fn,
|
|
1045
|
+
)
|
|
1046
|
+
)
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (postFilter === "go_path_traversal_sink") {
|
|
1051
|
+
const pkg = captures.PKG?.text ?? "";
|
|
1052
|
+
const fn = captures.FN?.text ?? "";
|
|
1053
|
+
if (!/^(os|ioutil)$/.test(pkg)) continue;
|
|
1054
|
+
if (!/^(Open|OpenFile|ReadFile|WriteFile|Create|Remove|RemoveAll)$/.test(fn))
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (postFilter === "py_sql_injection_sink") {
|
|
1059
|
+
const fn = captures.FN?.text ?? "";
|
|
1060
|
+
if (!/^(execute|executemany|query|raw)$/.test(fn)) continue;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (postFilter === "go_sql_injection_sink") {
|
|
1064
|
+
const dbFn = captures.DBFN?.text ?? "";
|
|
1065
|
+
const fmtPkg = captures.FMTPKG?.text ?? "";
|
|
1066
|
+
const fmtFn = captures.FMTFN?.text ?? "";
|
|
1067
|
+
if (!/^(Query|QueryContext|QueryRow|QueryRowContext|Exec|ExecContext)$/.test(dbFn))
|
|
1068
|
+
continue;
|
|
1069
|
+
if (fmtPkg !== "fmt" || fmtFn !== "Sprintf") continue;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (postFilter === "py_insecure_deserialization_sink") {
|
|
1073
|
+
const mod = captures.MOD?.text ?? "";
|
|
1074
|
+
const fn = captures.FN?.text ?? "";
|
|
1075
|
+
if (!/^(pickle|yaml)$/.test(mod)) continue;
|
|
1076
|
+
if (!/^(load|loads|unsafe_load)$/.test(fn)) continue;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (postFilter === "ruby_insecure_deserialization_sink") {
|
|
1080
|
+
const mod = captures.MOD?.text ?? "";
|
|
1081
|
+
const fn = captures.FN?.text ?? "";
|
|
1082
|
+
if (!/^(Marshal|YAML|Psych)$/.test(mod)) continue;
|
|
1083
|
+
if (!/^(load|unsafe_load)$/.test(fn)) continue;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
924
1086
|
// Use first capture for position info
|
|
925
1087
|
if (match.captures.length > 0) {
|
|
926
1088
|
const firstNode = match.captures[0].node;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
const TREE_SITTER_LOG_DIR = path.join(os.homedir(), ".pi-lens");
|
|
6
|
+
const TREE_SITTER_LOG_FILE = path.join(TREE_SITTER_LOG_DIR, "tree-sitter.log");
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
if (!fs.existsSync(TREE_SITTER_LOG_DIR)) {
|
|
10
|
+
fs.mkdirSync(TREE_SITTER_LOG_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
} catch {}
|
|
13
|
+
|
|
14
|
+
export interface TreeSitterLogEntry {
|
|
15
|
+
ts?: string;
|
|
16
|
+
phase:
|
|
17
|
+
| "runner_start"
|
|
18
|
+
| "runner_skip"
|
|
19
|
+
| "queries_loaded"
|
|
20
|
+
| "query_error"
|
|
21
|
+
| "runner_complete"
|
|
22
|
+
| "entity_diff"
|
|
23
|
+
| "blast_radius";
|
|
24
|
+
filePath: string;
|
|
25
|
+
languageId?: string;
|
|
26
|
+
queryId?: string;
|
|
27
|
+
status?: string;
|
|
28
|
+
diagnostics?: number;
|
|
29
|
+
blocking?: number;
|
|
30
|
+
queryCount?: number;
|
|
31
|
+
effectiveQueryCount?: number;
|
|
32
|
+
cacheHit?: boolean;
|
|
33
|
+
reason?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function logTreeSitter(entry: TreeSitterLogEntry): void {
|
|
39
|
+
const line = `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`;
|
|
40
|
+
try {
|
|
41
|
+
fs.appendFileSync(TREE_SITTER_LOG_FILE, line);
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getTreeSitterLogPath(): string {
|
|
46
|
+
return TREE_SITTER_LOG_FILE;
|
|
47
|
+
}
|
|
@@ -32,6 +32,9 @@ export interface TreeSitterQuery {
|
|
|
32
32
|
value: string | string[];
|
|
33
33
|
}>;
|
|
34
34
|
tags?: string[];
|
|
35
|
+
cwe?: string[];
|
|
36
|
+
owasp?: string[];
|
|
37
|
+
confidence?: "low" | "medium" | "high";
|
|
35
38
|
defect_class?: string;
|
|
36
39
|
inline_tier?: "blocking" | "warning" | "review";
|
|
37
40
|
has_fix: boolean;
|
|
@@ -172,6 +175,13 @@ export class TreeSitterQueryLoader {
|
|
|
172
175
|
}))
|
|
173
176
|
: undefined,
|
|
174
177
|
tags: Array.isArray(parsed.tags) ? parsed.tags.map(String) : undefined,
|
|
178
|
+
cwe: Array.isArray(parsed.cwe) ? parsed.cwe.map(String) : undefined,
|
|
179
|
+
owasp: Array.isArray(parsed.owasp)
|
|
180
|
+
? parsed.owasp.map(String)
|
|
181
|
+
: undefined,
|
|
182
|
+
confidence: parsed.confidence
|
|
183
|
+
? (String(parsed.confidence) as "low" | "medium" | "high")
|
|
184
|
+
: undefined,
|
|
175
185
|
has_fix: parsed.has_fix === true || parsed.has_fix === "true",
|
|
176
186
|
fix_action: parsed.fix_action ? String(parsed.fix_action) : undefined,
|
|
177
187
|
filePath,
|
|
@@ -298,8 +308,9 @@ export class TreeSitterQueryLoader {
|
|
|
298
308
|
break;
|
|
299
309
|
}
|
|
300
310
|
|
|
301
|
-
// Skip comment lines
|
|
302
|
-
|
|
311
|
+
// Skip YAML comment lines for most keys, but preserve native
|
|
312
|
+
// tree-sitter predicate lines in query blocks (#eq?, #match?, ...).
|
|
313
|
+
if (trimmed.startsWith("#") && key !== "query") continue;
|
|
303
314
|
|
|
304
315
|
// This is part of the multiline value
|
|
305
316
|
valueLines.push(line.slice(startIndent));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"rust:build:debug": "cargo build --manifest-path rust/Cargo.toml",
|
|
23
23
|
"check": "node scripts/check-extensions.mjs",
|
|
24
24
|
"audit:tree-sitter": "node scripts/audit-tree-sitter-rules.mjs",
|
|
25
|
+
"audit:rule-catalog": "node scripts/validate-rule-catalog.mjs",
|
|
25
26
|
"download-grammars": "node scripts/download-grammars.js",
|
|
26
27
|
"postinstall": "node scripts/download-grammars.js"
|
|
27
28
|
},
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
"scripts/download-grammars.js",
|
|
54
55
|
"scripts/check-extensions.mjs",
|
|
55
56
|
"scripts/audit-tree-sitter-rules.mjs",
|
|
57
|
+
"scripts/validate-rule-catalog.mjs",
|
|
56
58
|
"rust/src/",
|
|
57
59
|
"rust/Cargo.toml",
|
|
58
60
|
"default-architect.yaml",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"entries": [
|
|
4
|
+
{ "rule_id": "go-command-injection", "engine": "tree-sitter", "language": "go", "family": "security", "scope": "file", "canonical_concept": "command-injection-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
5
|
+
{ "rule_id": "go-hardcoded-secrets", "engine": "tree-sitter", "language": "go", "family": "security", "scope": "file", "canonical_concept": "hardcoded-secrets", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
6
|
+
{ "rule_id": "go-insecure-random", "engine": "tree-sitter", "language": "go", "family": "security", "scope": "file", "canonical_concept": "insecure-randomness", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
7
|
+
{ "rule_id": "go-path-traversal", "engine": "tree-sitter", "language": "go", "family": "security", "scope": "file", "canonical_concept": "path-traversal-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
8
|
+
{ "rule_id": "go-sql-injection", "engine": "tree-sitter", "language": "go", "family": "security", "scope": "file", "canonical_concept": "sql-injection-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
9
|
+
{ "rule_id": "go-weak-hash", "engine": "tree-sitter", "language": "go", "family": "security", "scope": "file", "canonical_concept": "weak-hash-primitive", "severity_default": "warning", "confidence": "high", "status": "experimental" },
|
|
10
|
+
{ "rule_id": "python-command-injection", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "command-injection-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
11
|
+
{ "rule_id": "python-hardcoded-secrets", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "hardcoded-secrets", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
12
|
+
{ "rule_id": "eval-exec", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "dynamic-code-execution", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
13
|
+
{ "rule_id": "python-insecure-deserialization", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "insecure-deserialization-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
14
|
+
{ "rule_id": "python-insecure-random", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "insecure-randomness", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
15
|
+
{ "rule_id": "python-path-traversal", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "path-traversal-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
16
|
+
{ "rule_id": "python-sql-injection", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "sql-injection-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
17
|
+
{ "rule_id": "python-ssrf", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "ssrf-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
18
|
+
{ "rule_id": "python-unsafe-regex", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "unsafe-regex", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
19
|
+
{ "rule_id": "python-weak-hash", "engine": "tree-sitter", "language": "python", "family": "security", "scope": "file", "canonical_concept": "weak-hash-primitive", "severity_default": "warning", "confidence": "high", "status": "experimental" },
|
|
20
|
+
{ "rule_id": "ruby-command-injection", "engine": "tree-sitter", "language": "ruby", "family": "security", "scope": "file", "canonical_concept": "command-injection-sink", "severity_default": "warning", "confidence": "low", "status": "experimental" },
|
|
21
|
+
{ "rule_id": "ruby-eval", "engine": "tree-sitter", "language": "ruby", "family": "security", "scope": "file", "canonical_concept": "dynamic-code-execution", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
22
|
+
{ "rule_id": "ruby-hardcoded-secrets", "engine": "tree-sitter", "language": "ruby", "family": "security", "scope": "file", "canonical_concept": "hardcoded-secrets", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
23
|
+
{ "rule_id": "ruby-insecure-deserialization", "engine": "tree-sitter", "language": "ruby", "family": "security", "scope": "file", "canonical_concept": "insecure-deserialization-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
24
|
+
{ "rule_id": "ruby-insecure-random", "engine": "tree-sitter", "language": "ruby", "family": "security", "scope": "file", "canonical_concept": "insecure-randomness", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
25
|
+
{ "rule_id": "ruby-unsafe-regex", "engine": "tree-sitter", "language": "ruby", "family": "security", "scope": "file", "canonical_concept": "unsafe-regex", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
26
|
+
{ "rule_id": "ruby-weak-hash", "engine": "tree-sitter", "language": "ruby", "family": "security", "scope": "file", "canonical_concept": "weak-hash-primitive", "severity_default": "warning", "confidence": "high", "status": "experimental" },
|
|
27
|
+
{ "rule_id": "dangerously-set-inner-html", "engine": "tree-sitter", "language": "tsx", "family": "security", "scope": "file", "canonical_concept": "dom-xss-dangerous-inner-html", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
28
|
+
{ "rule_id": "no-eval", "engine": "tree-sitter", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "dynamic-code-execution", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
29
|
+
{ "rule_id": "hardcoded-secrets", "engine": "tree-sitter", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "hardcoded-secrets", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
30
|
+
{ "rule_id": "sql-injection", "engine": "tree-sitter", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "sql-injection-sink", "severity_default": "error", "confidence": "medium", "status": "active" },
|
|
31
|
+
{ "rule_id": "ts-command-injection", "engine": "tree-sitter", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "command-injection-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
32
|
+
{ "rule_id": "ts-insecure-random", "engine": "tree-sitter", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "insecure-randomness", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
33
|
+
{ "rule_id": "ts-ssrf", "engine": "tree-sitter", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "ssrf-sink", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
34
|
+
{ "rule_id": "ts-weak-hash", "engine": "tree-sitter", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "weak-hash-primitive", "severity_default": "warning", "confidence": "high", "status": "experimental" },
|
|
35
|
+
{ "rule_id": "unsafe-regex", "engine": "tree-sitter", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "unsafe-regex", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
36
|
+
{ "rule_id": "go-goroutine-loop-capture", "engine": "tree-sitter", "language": "go", "family": "concurrency", "scope": "file", "canonical_concept": "goroutine-loop-capture", "severity_default": "warning", "confidence": "low", "status": "experimental" },
|
|
37
|
+
{ "rule_id": "go-shared-map-write-goroutine", "engine": "tree-sitter", "language": "go", "family": "concurrency", "scope": "file", "canonical_concept": "shared-map-write-in-goroutine", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
38
|
+
{ "rule_id": "python-thread-global-write", "engine": "tree-sitter", "language": "python", "family": "concurrency", "scope": "file", "canonical_concept": "threaded-shared-state-write", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
39
|
+
{ "rule_id": "rust-lock-held-across-await", "engine": "tree-sitter", "language": "rust", "family": "concurrency", "scope": "file", "canonical_concept": "lock-held-across-await", "severity_default": "warning", "confidence": "low", "status": "experimental" },
|
|
40
|
+
{ "rule_id": "ts-detached-async-call", "engine": "tree-sitter", "language": "typescript", "family": "concurrency", "scope": "file", "canonical_concept": "detached-async-call", "severity_default": "warning", "confidence": "medium", "status": "experimental" },
|
|
41
|
+
|
|
42
|
+
{ "rule_id": "no-sql-in-code", "engine": "ast-grep", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "sql-in-code-literal", "severity_default": "warning", "confidence": "medium", "status": "active" },
|
|
43
|
+
{ "rule_id": "no-sql-in-code-js", "engine": "ast-grep", "language": "javascript", "family": "security", "scope": "file", "canonical_concept": "sql-in-code-literal", "severity_default": "warning", "confidence": "medium", "status": "active" },
|
|
44
|
+
{ "rule_id": "no-open-redirect", "engine": "ast-grep", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "open-redirect", "severity_default": "warning", "confidence": "medium", "status": "active" },
|
|
45
|
+
{ "rule_id": "no-open-redirect-js", "engine": "ast-grep", "language": "javascript", "family": "security", "scope": "file", "canonical_concept": "open-redirect", "severity_default": "warning", "confidence": "medium", "status": "active" },
|
|
46
|
+
{ "rule_id": "no-javascript-url", "engine": "ast-grep", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "javascript-url-scheme", "severity_default": "warning", "confidence": "high", "status": "active" },
|
|
47
|
+
{ "rule_id": "no-javascript-url-js", "engine": "ast-grep", "language": "javascript", "family": "security", "scope": "file", "canonical_concept": "javascript-url-scheme", "severity_default": "warning", "confidence": "high", "status": "active" },
|
|
48
|
+
{ "rule_id": "no-insecure-randomness", "engine": "ast-grep", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "insecure-randomness", "severity_default": "warning", "confidence": "medium", "status": "active" },
|
|
49
|
+
{ "rule_id": "no-insecure-randomness-js", "engine": "ast-grep", "language": "javascript", "family": "security", "scope": "file", "canonical_concept": "insecure-randomness", "severity_default": "warning", "confidence": "medium", "status": "active" },
|
|
50
|
+
{ "rule_id": "no-implied-eval", "engine": "ast-grep", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "dynamic-code-execution", "severity_default": "error", "confidence": "high", "status": "active", "allow_overlap": true },
|
|
51
|
+
{ "rule_id": "no-implied-eval-js", "engine": "ast-grep", "language": "javascript", "family": "security", "scope": "file", "canonical_concept": "dynamic-code-execution", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
52
|
+
{ "rule_id": "no-hardcoded-secrets", "engine": "ast-grep", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "hardcoded-secrets", "severity_default": "error", "confidence": "high", "status": "active", "allow_overlap": true },
|
|
53
|
+
{ "rule_id": "no-hardcoded-secrets-js", "engine": "ast-grep", "language": "javascript", "family": "security", "scope": "file", "canonical_concept": "hardcoded-secrets", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
54
|
+
{ "rule_id": "no-global-eval-js", "engine": "ast-grep", "language": "javascript", "family": "security", "scope": "file", "canonical_concept": "dynamic-code-execution", "severity_default": "error", "confidence": "high", "status": "active", "allow_overlap": true },
|
|
55
|
+
{ "rule_id": "jwt-no-verify", "engine": "ast-grep", "language": "typescript", "family": "security", "scope": "file", "canonical_concept": "jwt-signature-bypass", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
56
|
+
{ "rule_id": "jwt-no-verify-js", "engine": "ast-grep", "language": "javascript", "family": "security", "scope": "file", "canonical_concept": "jwt-signature-bypass", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
57
|
+
{ "rule_id": "toctou", "engine": "ast-grep", "language": "typescript", "family": "concurrency", "scope": "file", "canonical_concept": "toctou-file-race", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
58
|
+
{ "rule_id": "toctou-js", "engine": "ast-grep", "language": "javascript", "family": "concurrency", "scope": "file", "canonical_concept": "toctou-file-race", "severity_default": "error", "confidence": "high", "status": "active" },
|
|
59
|
+
{ "rule_id": "missed-concurrency", "engine": "ast-grep", "language": "typescript", "family": "concurrency", "scope": "function", "canonical_concept": "sequential-awaits-parallelizable", "severity_default": "warning", "confidence": "medium", "status": "active" },
|
|
60
|
+
{ "rule_id": "missed-concurrency-js", "engine": "ast-grep", "language": "javascript", "family": "concurrency", "scope": "function", "canonical_concept": "sequential-awaits-parallelizable", "severity_default": "warning", "confidence": "medium", "status": "active" },
|
|
61
|
+
{ "rule_id": "no-await-in-loop", "engine": "ast-grep", "language": "typescript", "family": "concurrency", "scope": "function", "canonical_concept": "await-inside-loop", "severity_default": "warning", "confidence": "medium", "status": "active" },
|
|
62
|
+
{ "rule_id": "no-await-in-loop-js", "engine": "ast-grep", "language": "javascript", "family": "concurrency", "scope": "function", "canonical_concept": "await-inside-loop", "severity_default": "warning", "confidence": "medium", "status": "active" }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -16,17 +16,29 @@ description: |
|
|
|
16
16
|
✅ FIX: Handle the error before returning, or document why it's safe.
|
|
17
17
|
|
|
18
18
|
query: |
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
(
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
[
|
|
20
|
+
(function_declaration
|
|
21
|
+
result: (type_identifier) @RET
|
|
22
|
+
body: (block
|
|
23
|
+
(return_statement
|
|
24
|
+
(expression_list
|
|
25
|
+
(call_expression) @CALL)))
|
|
26
|
+
(#eq? @RET "error"))
|
|
27
|
+
(function_declaration
|
|
28
|
+
result: (parameter_list
|
|
29
|
+
(parameter_declaration
|
|
30
|
+
type: (type_identifier) @RET))
|
|
31
|
+
body: (block
|
|
32
|
+
(return_statement
|
|
33
|
+
(expression_list
|
|
34
|
+
(call_expression) @CALL)))
|
|
35
|
+
(#eq? @RET "error"))
|
|
36
|
+
]
|
|
24
37
|
|
|
25
38
|
metavars:
|
|
39
|
+
- RET
|
|
26
40
|
- CALL
|
|
27
41
|
|
|
28
|
-
post_filter: returns_error
|
|
29
|
-
|
|
30
42
|
has_fix: false
|
|
31
43
|
|
|
32
44
|
tags:
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Go Security
|
|
2
|
+
# Detects shell command execution via exec.Command(..., "-c"|"/c", ...).
|
|
3
|
+
id: go-command-injection
|
|
4
|
+
name: Command Injection Sink
|
|
5
|
+
severity: warning
|
|
6
|
+
category: security
|
|
7
|
+
defect_class: injection
|
|
8
|
+
inline_tier: warning
|
|
9
|
+
language: go
|
|
10
|
+
|
|
11
|
+
message: "Potential command injection sink — avoid shell command composition with user input"
|
|
12
|
+
|
|
13
|
+
description: |
|
|
14
|
+
Using exec.Command with a shell (`sh -c`, `bash -c`, `cmd /c`) executes command strings.
|
|
15
|
+
|
|
16
|
+
✅ FIX: invoke binaries directly with explicit argument arrays and strict allowlists.
|
|
17
|
+
|
|
18
|
+
query: |
|
|
19
|
+
(call_expression
|
|
20
|
+
function: (selector_expression
|
|
21
|
+
operand: (identifier) @PKG
|
|
22
|
+
field: (field_identifier) @FN)
|
|
23
|
+
arguments: (argument_list
|
|
24
|
+
(interpreted_string_literal) @SHELL
|
|
25
|
+
(interpreted_string_literal) @FLAG
|
|
26
|
+
(_) @CMD))
|
|
27
|
+
(#eq? @PKG "exec")
|
|
28
|
+
(#match? @FN "^(Command|CommandContext)$")
|
|
29
|
+
(#match? @SHELL "^\"(sh|bash|zsh|cmd|powershell|pwsh)\"$")
|
|
30
|
+
(#match? @FLAG "^\"(-c|/c)\"$")
|
|
31
|
+
|
|
32
|
+
metavars:
|
|
33
|
+
- PKG
|
|
34
|
+
- FN
|
|
35
|
+
- SHELL
|
|
36
|
+
- FLAG
|
|
37
|
+
- CMD
|
|
38
|
+
|
|
39
|
+
post_filter: go_command_injection_sink
|
|
40
|
+
|
|
41
|
+
has_fix: false
|
|
42
|
+
|
|
43
|
+
tags:
|
|
44
|
+
- go
|
|
45
|
+
- security
|
|
46
|
+
- command-injection
|
|
47
|
+
- cwe-78
|
|
48
|
+
- owasp-a03
|
|
49
|
+
|
|
50
|
+
examples:
|
|
51
|
+
bad: |
|
|
52
|
+
exec.Command("sh", "-c", userInput)
|
|
53
|
+
|
|
54
|
+
good: |
|
|
55
|
+
exec.Command("git", "status")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Go Reliability
|
|
2
|
+
# Detects direct panic calls in application/library code.
|
|
3
|
+
id: go-direct-panic
|
|
4
|
+
name: Direct panic() Call
|
|
5
|
+
severity: error
|
|
6
|
+
category: reliability
|
|
7
|
+
defect_class: safety
|
|
8
|
+
inline_tier: blocking
|
|
9
|
+
language: go
|
|
10
|
+
|
|
11
|
+
message: "Direct panic() call can crash the process — return or propagate an error instead"
|
|
12
|
+
|
|
13
|
+
description: |
|
|
14
|
+
Calling `panic(...)` for ordinary error paths can terminate the process and bypass
|
|
15
|
+
normal recovery/cleanup flow.
|
|
16
|
+
|
|
17
|
+
✅ FIX: return an error, wrap it, or handle it at the boundary layer.
|
|
18
|
+
|
|
19
|
+
query: |
|
|
20
|
+
(call_expression
|
|
21
|
+
function: (identifier) @FN
|
|
22
|
+
arguments: (argument_list) @ARGS)
|
|
23
|
+
(#eq? @FN "panic")
|
|
24
|
+
|
|
25
|
+
metavars:
|
|
26
|
+
- FN
|
|
27
|
+
- ARGS
|
|
28
|
+
|
|
29
|
+
has_fix: false
|
|
30
|
+
|
|
31
|
+
tags:
|
|
32
|
+
- go
|
|
33
|
+
- reliability
|
|
34
|
+
- safety
|
|
35
|
+
|
|
36
|
+
examples:
|
|
37
|
+
bad: |
|
|
38
|
+
if err != nil {
|
|
39
|
+
panic(err)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
good: |
|
|
43
|
+
if err != nil {
|
|
44
|
+
return fmt.Errorf("failed: %w", err)
|
|
45
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Go Error Handling
|
|
2
|
+
# Detects empty `if err != nil` handlers
|
|
3
|
+
id: go-empty-if-err
|
|
4
|
+
name: Empty Error Handler
|
|
5
|
+
severity: error
|
|
6
|
+
category: error-handling
|
|
7
|
+
defect_class: silent-error
|
|
8
|
+
inline_tier: blocking
|
|
9
|
+
language: go
|
|
10
|
+
|
|
11
|
+
message: "Empty error handler — handle err or return it"
|
|
12
|
+
|
|
13
|
+
description: |
|
|
14
|
+
An `if err != nil {}` block silently ignores failures.
|
|
15
|
+
|
|
16
|
+
✅ FIX: return, wrap, or log the error with context.
|
|
17
|
+
|
|
18
|
+
query: |
|
|
19
|
+
(if_statement
|
|
20
|
+
condition: (binary_expression
|
|
21
|
+
left: (identifier) @ERR
|
|
22
|
+
right: (nil))
|
|
23
|
+
consequence: (block) @BODY)
|
|
24
|
+
(#eq? @ERR "err")
|
|
25
|
+
|
|
26
|
+
metavars:
|
|
27
|
+
- ERR
|
|
28
|
+
- BODY
|
|
29
|
+
|
|
30
|
+
post_filter: empty_body
|
|
31
|
+
|
|
32
|
+
has_fix: false
|
|
33
|
+
|
|
34
|
+
tags:
|
|
35
|
+
- go
|
|
36
|
+
- error-handling
|
|
37
|
+
- reliability
|
|
38
|
+
|
|
39
|
+
examples:
|
|
40
|
+
bad: |
|
|
41
|
+
if err != nil {
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
good: |
|
|
45
|
+
if err != nil {
|
|
46
|
+
return fmt.Errorf("load config: %w", err)
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Go Concurrency
|
|
2
|
+
# Detects goroutines started inside loops with parameterless func literals.
|
|
3
|
+
id: go-goroutine-loop-capture
|
|
4
|
+
name: Goroutine Loop Capture Risk
|
|
5
|
+
severity: warning
|
|
6
|
+
category: concurrency
|
|
7
|
+
defect_class: async-misuse
|
|
8
|
+
inline_tier: warning
|
|
9
|
+
language: go
|
|
10
|
+
|
|
11
|
+
message: "Goroutine launched in loop with captured variables — pass loop values as parameters"
|
|
12
|
+
|
|
13
|
+
description: |
|
|
14
|
+
Launching `go func(){...}()` inside loops can capture changing loop variables.
|
|
15
|
+
|
|
16
|
+
✅ FIX: pass loop variables as explicit function parameters.
|
|
17
|
+
|
|
18
|
+
query: |
|
|
19
|
+
(for_statement
|
|
20
|
+
body: (block
|
|
21
|
+
(go_statement) @GO)
|
|
22
|
+
|
|
23
|
+
metavars:
|
|
24
|
+
- GO
|
|
25
|
+
|
|
26
|
+
cwe:
|
|
27
|
+
- CWE-362
|
|
28
|
+
owasp:
|
|
29
|
+
- A09
|
|
30
|
+
confidence: medium
|
|
31
|
+
|
|
32
|
+
has_fix: false
|
|
33
|
+
|
|
34
|
+
tags:
|
|
35
|
+
- go
|
|
36
|
+
- concurrency
|
|
37
|
+
- goroutine
|
|
38
|
+
- loop-capture
|
|
39
|
+
|
|
40
|
+
examples:
|
|
41
|
+
bad: |
|
|
42
|
+
for _, item := range items {
|
|
43
|
+
go func() { use(item) }()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
good: |
|
|
47
|
+
for _, item := range items {
|
|
48
|
+
go func(v string) { use(v) }(item)
|
|
49
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Go Reliability
|
|
2
|
+
# Detects discarded call results using blank identifier assignment.
|
|
3
|
+
id: go-ignored-call-result
|
|
4
|
+
name: Ignored Call Result
|
|
5
|
+
severity: warning
|
|
6
|
+
category: error-handling
|
|
7
|
+
defect_class: silent-error
|
|
8
|
+
inline_tier: warning
|
|
9
|
+
language: go
|
|
10
|
+
|
|
11
|
+
message: "Call result assigned to '_' — verify this discard is intentional"
|
|
12
|
+
|
|
13
|
+
description: |
|
|
14
|
+
Assigning a call result to `_` can hide failures or important return values.
|
|
15
|
+
|
|
16
|
+
✅ FIX: handle the returned value/error or document intentional discard.
|
|
17
|
+
|
|
18
|
+
query: |
|
|
19
|
+
[
|
|
20
|
+
(short_var_declaration
|
|
21
|
+
left: (expression_list
|
|
22
|
+
(identifier) @VAR)
|
|
23
|
+
right: (expression_list
|
|
24
|
+
(call_expression) @CALL))
|
|
25
|
+
(assignment_statement
|
|
26
|
+
left: (expression_list
|
|
27
|
+
(identifier) @VAR)
|
|
28
|
+
right: (expression_list
|
|
29
|
+
(call_expression) @CALL))
|
|
30
|
+
]
|
|
31
|
+
(#eq? @VAR "_")
|
|
32
|
+
|
|
33
|
+
metavars:
|
|
34
|
+
- VAR
|
|
35
|
+
- CALL
|
|
36
|
+
|
|
37
|
+
has_fix: false
|
|
38
|
+
|
|
39
|
+
tags:
|
|
40
|
+
- go
|
|
41
|
+
- reliability
|
|
42
|
+
- code-smell
|
|
43
|
+
|
|
44
|
+
examples:
|
|
45
|
+
bad: |
|
|
46
|
+
_ = doWork()
|
|
47
|
+
|
|
48
|
+
good: |
|
|
49
|
+
if err := doWork(); err != nil {
|
|
50
|
+
return err
|
|
51
|
+
}
|