pi-lens 3.8.21 → 3.8.23

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 (92) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +2 -0
  3. package/clients/dispatch/dispatcher.ts +75 -91
  4. package/clients/dispatch/fact-provider-types.ts +22 -0
  5. package/clients/dispatch/fact-rule-runner.ts +22 -0
  6. package/clients/dispatch/fact-runner.ts +28 -0
  7. package/clients/dispatch/fact-scheduler.ts +78 -0
  8. package/clients/dispatch/fact-store.ts +67 -0
  9. package/clients/dispatch/facts/comment-facts.ts +59 -0
  10. package/clients/dispatch/facts/file-content.ts +20 -0
  11. package/clients/dispatch/facts/function-facts.ts +177 -0
  12. package/clients/dispatch/facts/try-catch-facts.ts +80 -0
  13. package/clients/dispatch/integration.ts +130 -24
  14. package/clients/dispatch/priorities.ts +22 -0
  15. package/clients/dispatch/rules/async-noise.ts +43 -0
  16. package/clients/dispatch/rules/error-obscuring.ts +40 -0
  17. package/clients/dispatch/rules/error-swallowing.ts +35 -0
  18. package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
  19. package/clients/dispatch/rules/placeholder-comments.ts +47 -0
  20. package/clients/dispatch/runners/architect.ts +2 -1
  21. package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
  22. package/clients/dispatch/runners/biome-check.ts +40 -8
  23. package/clients/dispatch/runners/biome.ts +2 -1
  24. package/clients/dispatch/runners/eslint.ts +34 -6
  25. package/clients/dispatch/runners/go-vet.ts +2 -1
  26. package/clients/dispatch/runners/golangci-lint.ts +2 -1
  27. package/clients/dispatch/runners/index.ts +29 -27
  28. package/clients/dispatch/runners/lsp.ts +60 -4
  29. package/clients/dispatch/runners/oxlint.ts +2 -1
  30. package/clients/dispatch/runners/pyright.ts +2 -1
  31. package/clients/dispatch/runners/python-slop.ts +2 -1
  32. package/clients/dispatch/runners/rubocop.ts +2 -1
  33. package/clients/dispatch/runners/ruff.ts +2 -1
  34. package/clients/dispatch/runners/rust-clippy.ts +2 -1
  35. package/clients/dispatch/runners/shellcheck.ts +2 -1
  36. package/clients/dispatch/runners/similarity.ts +2 -1
  37. package/clients/dispatch/runners/spellcheck.ts +2 -1
  38. package/clients/dispatch/runners/sqlfluff.ts +2 -1
  39. package/clients/dispatch/runners/tree-sitter.ts +469 -1
  40. package/clients/dispatch/runners/ts-lsp.ts +2 -1
  41. package/clients/dispatch/runners/type-safety.ts +2 -1
  42. package/clients/dispatch/runners/yamllint.ts +2 -1
  43. package/clients/dispatch/tool-profile.ts +40 -0
  44. package/clients/dispatch/types.ts +3 -13
  45. package/clients/lsp/client.ts +366 -12
  46. package/clients/lsp/index.ts +374 -76
  47. package/clients/lsp/launch.ts +42 -2
  48. package/clients/lsp/server.ts +186 -12
  49. package/clients/pipeline.ts +2 -2
  50. package/clients/runtime-context.ts +2 -2
  51. package/clients/runtime-session.ts +43 -5
  52. package/clients/session-summary.ts +21 -0
  53. package/clients/tree-sitter-client.ts +162 -0
  54. package/clients/tree-sitter-logger.ts +47 -0
  55. package/clients/tree-sitter-query-loader.ts +13 -2
  56. package/index.ts +67 -17
  57. package/package.json +3 -1
  58. package/rules/rule-catalog.json +64 -0
  59. package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
  60. package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
  61. package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
  62. package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
  63. package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
  64. package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
  65. package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
  66. package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
  67. package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
  68. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
  69. package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
  70. package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
  71. package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
  72. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
  73. package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
  74. package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
  75. package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
  76. package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
  77. package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
  78. package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
  79. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
  80. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
  81. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
  82. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
  83. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
  84. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
  85. package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
  86. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
  87. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
  88. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
  89. package/scripts/validate-rule-catalog.mjs +227 -0
  90. package/skills/lsp-navigation/SKILL.md +15 -3
  91. package/tools/lsp-navigation.js +466 -79
  92. package/tools/lsp-navigation.ts +587 -85
@@ -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/index.ts CHANGED
@@ -13,6 +13,7 @@ import { ComplexityClient } from "./clients/complexity-client.js";
13
13
  import { DependencyChecker } from "./clients/dependency-checker.js";
14
14
  import { getDiagnosticTracker } from "./clients/diagnostic-tracker.js";
15
15
  import {
16
+ getDispatchSlopScoreLine,
16
17
  getLatencyReports,
17
18
  resetDispatchBaselines,
18
19
  } from "./clients/dispatch/integration.js";
@@ -378,6 +379,7 @@ export default function (pi: ExtensionAPI) {
378
379
  `Pipeline crashes (session): ${totalCrashes}`,
379
380
  `Files affected: ${crashEntries.length}`,
380
381
  ];
382
+ const slopScoreLine = getDispatchSlopScoreLine();
381
383
 
382
384
  if (crashEntries.length > 0) {
383
385
  lines.push("", "Top crash files:");
@@ -432,6 +434,10 @@ export default function (pi: ExtensionAPI) {
432
434
  }
433
435
  }
434
436
 
437
+ if (slopScoreLine) {
438
+ lines.push("", slopScoreLine);
439
+ }
440
+
435
441
  ctx.ui.notify(lines.join("\n"), "info");
436
442
  },
437
443
  });
@@ -525,10 +531,20 @@ pi.on("tool_call", async (event, ctx) => {
525
531
  }
526
532
  }
527
533
 
528
- const filePath =
534
+ const inputObj = (event.input ?? {}) as Record<string, unknown>;
535
+ const rawFilePath =
529
536
  isToolCallEventType("write", event) || isToolCallEventType("edit", event)
530
537
  ? (event.input as { path: string }).path
531
- : undefined;
538
+ : toolName === "read" || toolName === "lsp_navigation"
539
+ ? typeof inputObj.filePath === "string"
540
+ ? inputObj.filePath
541
+ : undefined
542
+ : undefined;
543
+ const filePath = rawFilePath
544
+ ? path.isAbsolute(rawFilePath)
545
+ ? rawFilePath
546
+ : path.resolve(ctx.cwd ?? runtime.projectRoot, rawFilePath)
547
+ : undefined;
532
548
 
533
549
  if (!filePath) return;
534
550
 
@@ -537,6 +553,24 @@ pi.on("tool_call", async (event, ctx) => {
537
553
  );
538
554
  if (!nodeFs.existsSync(filePath)) return;
539
555
 
556
+ const shouldAutoTouch =
557
+ (toolName === "read" ||
558
+ toolName === "write" ||
559
+ toolName === "edit" ||
560
+ toolName === "lsp_navigation") &&
561
+ !!pi.getFlag("lens-lsp") &&
562
+ !pi.getFlag("no-lsp");
563
+ if (shouldAutoTouch) {
564
+ try {
565
+ const fileContent = nodeFs.readFileSync(filePath, "utf-8");
566
+ void getLSPService()
567
+ .touchFile(filePath, fileContent, false, `tool_call:${toolName}`)
568
+ .catch((err) => dbg(`lsp auto-touch failed for ${filePath}: ${err}`));
569
+ } catch {
570
+ // Best effort only; never block tool calls.
571
+ }
572
+ }
573
+
540
574
  // Record complexity baseline for historical tracking (booboo/tdi).
541
575
  // Not shown inline — just captured for delta analysis.
542
576
  if (
@@ -734,20 +768,36 @@ pi.on("turn_end", async (_event, ctx) => {
734
768
  // --- Inject turn-end findings into next agent turn ---
735
769
  // jscpd, madge, and turn-end delta results are cached at turn_end and consumed here
736
770
  // via the context event, which fires before each provider request.
771
+ // Important: context handlers must APPEND to the existing message list, not replace it.
772
+ // Replacing `event.messages` can drop the user's first prompt entirely, which causes
773
+ // OpenAI Responses requests to fail with: "One of input/previous_response_id/prompt/conversation_id must be provided."
737
774
  // biome-ignore lint/suspicious/noExplicitAny: pi.on("context") overload has TS resolution bug
738
- (pi as any).on("context", async (_event: unknown, ctx: { cwd?: string }) => {
739
- try {
740
- const cwd = ctx.cwd ?? process.cwd();
741
- const turnEndFindings = consumeTurnEndFindings(cacheManager, cwd);
742
- const sessionGuidance = consumeSessionStartGuidance(cacheManager, cwd);
743
- const messages = [
744
- ...(sessionGuidance?.messages ?? []),
745
- ...(turnEndFindings?.messages ?? []),
746
- ];
747
- if (messages.length === 0) return;
748
- return { messages };
749
- } catch (err) {
750
- dbg(`context event error: ${err}`);
751
- }
752
- });
775
+ (pi as any).on(
776
+ "context",
777
+ async (
778
+ event: { messages?: Array<{ role: string; content: unknown }> } | unknown,
779
+ ctx: { cwd?: string },
780
+ ) => {
781
+ try {
782
+ const cwd = ctx.cwd ?? process.cwd();
783
+ const turnEndFindings = consumeTurnEndFindings(cacheManager, cwd);
784
+ const sessionGuidance = consumeSessionStartGuidance(cacheManager, cwd);
785
+ const injectedMessages = [
786
+ ...(sessionGuidance?.messages ?? []),
787
+ ...(turnEndFindings?.messages ?? []),
788
+ ];
789
+ if (injectedMessages.length === 0) return;
790
+
791
+ const existingMessages =
792
+ (event as { messages?: Array<{ role: string; content: unknown }> })
793
+ ?.messages ?? [];
794
+
795
+ return {
796
+ messages: [...existingMessages, ...injectedMessages],
797
+ };
798
+ } catch (err) {
799
+ dbg(`context event error: ${err}`);
800
+ }
801
+ },
802
+ );
753
803
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "3.8.21",
3
+ "version": "3.8.23",
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
+ }
@@ -0,0 +1,51 @@
1
+ # Go Security
2
+ # Detects use of math/rand in potentially security-sensitive contexts.
3
+ id: go-insecure-random
4
+ name: Insecure Randomness
5
+ severity: warning
6
+ category: security
7
+ defect_class: injection
8
+ inline_tier: warning
9
+ language: go
10
+
11
+ message: "Insecure randomness source detected — use crypto/rand for security-sensitive values"
12
+
13
+ description: |
14
+ math/rand is deterministic and not suitable for security-sensitive randomness.
15
+
16
+ ✅ FIX: use crypto/rand for tokens, keys, nonces, and secrets.
17
+
18
+ query: |
19
+ (call_expression
20
+ function: (selector_expression
21
+ operand: (identifier) @PKG
22
+ field: (field_identifier) @FN)
23
+ arguments: (argument_list) @ARGS)
24
+ (#eq? @PKG "rand")
25
+ (#match? @FN "^(Int|Intn|Int63|Uint32|Uint64|Read|Float64)$")
26
+
27
+ metavars:
28
+ - PKG
29
+ - FN
30
+ - ARGS
31
+
32
+ cwe:
33
+ - CWE-330
34
+ owasp:
35
+ - A02
36
+ confidence: medium
37
+
38
+ has_fix: false
39
+
40
+ tags:
41
+ - go
42
+ - security
43
+ - randomness
44
+ - weak-prng
45
+
46
+ examples:
47
+ bad: |
48
+ code := rand.Intn(1000000)
49
+
50
+ good: |
51
+ _, _ = rand.Read(buf) // from crypto/rand