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.
Files changed (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -0
  3. package/clients/dispatch/runners/lsp.ts +58 -3
  4. package/clients/dispatch/runners/tree-sitter.ts +467 -0
  5. package/clients/lsp/client.ts +229 -3
  6. package/clients/lsp/index.ts +111 -1
  7. package/clients/pipeline.ts +2 -2
  8. package/clients/runtime-session.ts +43 -5
  9. package/clients/tree-sitter-client.ts +162 -0
  10. package/clients/tree-sitter-logger.ts +47 -0
  11. package/clients/tree-sitter-query-loader.ts +13 -2
  12. package/package.json +3 -1
  13. package/rules/rule-catalog.json +64 -0
  14. package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
  15. package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
  16. package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
  17. package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
  18. package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
  19. package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
  20. package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
  21. package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
  22. package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
  23. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
  24. package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
  25. package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
  26. package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
  27. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
  28. package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
  29. package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
  30. package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
  31. package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
  32. package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
  33. package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
  34. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
  35. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
  36. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
  37. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
  38. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
  39. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
  40. package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
  41. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
  42. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
  43. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
  44. package/scripts/validate-rule-catalog.mjs +227 -0
  45. package/skills/lsp-navigation/SKILL.md +15 -3
  46. package/tools/lsp-navigation.js +259 -28
  47. 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 (they're not part of the value)
302
- if (trimmed.startsWith("#")) continue;
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.21",
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
- (function_declaration
20
- body: (block
21
- (return_statement
22
- (expression_list
23
- (call_expression) @CALL))))
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
+ }