@kevinrabun/judges 3.119.0 → 3.122.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/api.d.ts +2 -1
  3. package/dist/api.js +3 -1
  4. package/dist/cli-dispatch.d.ts +7 -0
  5. package/dist/cli-dispatch.js +654 -0
  6. package/dist/cli-formatters.d.ts +6 -0
  7. package/dist/cli-formatters.js +186 -0
  8. package/dist/cli.js +69 -4159
  9. package/dist/commands/baseline.js +2 -42
  10. package/dist/commands/coverage.js +3 -39
  11. package/dist/commands/diff.js +2 -38
  12. package/dist/commands/fix-pr.js +2 -23
  13. package/dist/commands/fix.js +3 -27
  14. package/dist/commands/llm-benchmark.d.ts +7 -0
  15. package/dist/commands/llm-benchmark.js +27 -1
  16. package/dist/commands/quality-gate.js +1 -12
  17. package/dist/commands/review-parallel.js +1 -19
  18. package/dist/commands/review.js +2 -33
  19. package/dist/commands/rule-test.js +1 -15
  20. package/dist/commands/tune.js +2 -29
  21. package/dist/commands/watch.js +3 -42
  22. package/dist/config.js +1 -1
  23. package/dist/evaluators/hallucination-detection.js +343 -0
  24. package/dist/evaluators/index.d.ts +2 -11
  25. package/dist/evaluators/index.js +3 -181
  26. package/dist/evaluators/security.js +226 -2
  27. package/dist/evaluators/suppressions.d.ts +49 -0
  28. package/dist/evaluators/suppressions.js +185 -0
  29. package/dist/ext-to-lang.d.ts +16 -0
  30. package/dist/ext-to-lang.js +60 -0
  31. package/dist/github-app.d.ts +1 -3
  32. package/dist/github-app.js +2 -34
  33. package/dist/parallel.js +2 -14
  34. package/dist/probabilistic/llm-response-validator.js +1 -1
  35. package/dist/reports/public-repo-report.js +9 -1
  36. package/dist/skill-loader.js +9 -6
  37. package/dist/tools/register-evaluation.js +2 -29
  38. package/package.json +1 -1
  39. package/server.json +2 -2
  40. package/src/skill-loader.ts +9 -6
@@ -213,6 +213,16 @@ export function analyzeSecurity(code, language) {
213
213
  /(?:req\.|request\.|params\.|query\.|body\.|args\.|input)/i.test(line)) {
214
214
  ssrfLines.push(i + 1);
215
215
  }
216
+ // Java: new URL(userInput).openConnection() or URL constructed from request parameter
217
+ if (/\bnew\s+URL\s*\(/i.test(line) &&
218
+ /(?:req\.|request\.|getParameter|params|query|body|args|input)/i.test(line)) {
219
+ ssrfLines.push(i + 1);
220
+ }
221
+ // Ruby: URI.open / Kernel.open with user input
222
+ if (/\b(?:URI\.open|Kernel\.open|open\()\s*/i.test(line) &&
223
+ /(?:params\[|request\.|args|input|user|url)/i.test(line)) {
224
+ ssrfLines.push(i + 1);
225
+ }
216
226
  // Indirect: variable assigned from req, then used in fetch
217
227
  if (/\b(?:fetch|axios|http\.get|https\.get|requests\.get|requests\.request)\s*\(\s*(\w+)/i.test(line)) {
218
228
  const match = line.match(/\b(?:fetch|axios|http\.get|requests\.get)\s*\(\s*(\w+)/i);
@@ -555,6 +565,16 @@ export function analyzeSecurity(code, language) {
555
565
  cmdInjLines.push(i + 1);
556
566
  }
557
567
  }
568
+ // Python subprocess with shell=True and user input
569
+ if (/\bsubprocess\.(?:run|call|Popen|check_output|check_call)\s*\(/i.test(line)) {
570
+ const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 5)).join("\n");
571
+ if (/shell\s*=\s*True/i.test(ctx)) {
572
+ // Check for user input in the command string (f-string, format, concatenation)
573
+ if (/(?:f["']|\$\{|\.format\s*\(|\+\s*\w|request\.|args\.get|params|query|body|input)/i.test(ctx)) {
574
+ cmdInjLines.push(i + 1);
575
+ }
576
+ }
577
+ }
558
578
  // PHP system/exec/passthru/shell_exec with user input variables
559
579
  if (/\b(?:system|exec|passthru|shell_exec|popen)\s*\(/i.test(line) &&
560
580
  /\$_(?:GET|POST|REQUEST)\[|(?:\.\s*\$|\$\w+)/i.test(line)) {
@@ -689,10 +709,17 @@ export function analyzeSecurity(code, language) {
689
709
  const cryptoMiscLines = [];
690
710
  for (let i = 0; i < lines.length; i++) {
691
711
  const line = lines[i];
692
- // Static/hardcoded IV
693
- if (/(?:static\s*IV|\b(?:iv|IV)\b\s*[:=]\s*(?:\[\]byte\s*\(|["'[])|var\s+\w*[Ii][Vv]\s*=)/i.test(line)) {
712
+ // Static/hardcoded IV — matches variable names containing iv/IV with hardcoded values
713
+ if (/(?:static\s*IV|\b(?:iv|IV)\b\s*[:=]\s*(?:\[\]byte\s*\(|["'[])| var\s+\w*[Ii][Vv]\s*=)/i.test(line)) {
694
714
  cryptoMiscLines.push(i + 1);
695
715
  }
716
+ // Broader IV detection: const/let/var STATIC_IV =, nonce = "...", etc.
717
+ if (/\b(?:const|let|var|val)\s+\w*(?:_iv|_IV|IV|Iv|_nonce|NONCE)\w*\s*=/.test(line)) {
718
+ // Must be assigned a hardcoded value (string, buffer, byte array)
719
+ if (/(?:Buffer\.from|new\s+Uint8Array|\[\]byte|"[^"]+"|'[^']+'|\[\s*\d)/.test(line)) {
720
+ cryptoMiscLines.push(i + 1);
721
+ }
722
+ }
696
723
  // ECB-like mode: manual block-by-block encryption without chain/GCM
697
724
  if (/block\.Encrypt\s*\(/i.test(line)) {
698
725
  const ctx = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 5)).join("\n");
@@ -700,6 +727,17 @@ export function analyzeSecurity(code, language) {
700
727
  cryptoMiscLines.push(i + 1);
701
728
  }
702
729
  }
730
+ // ECB mode explicitly selected
731
+ if (/['"](?:aes-\d+-ecb|ECB|DES-ECB|des-ecb)['"]|cipher\.NewCipher\b/i.test(line)) {
732
+ const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 3)).join("\n");
733
+ if (!/GCM|NewGCM|AEAD/i.test(ctx)) {
734
+ cryptoMiscLines.push(i + 1);
735
+ }
736
+ }
737
+ // DES/3DES/RC4 usage (known broken ciphers)
738
+ if (/['"](?:des(?:-ede3)?(?:-cbc|-ecb)?|rc4|RC4)['"]|DES\.(?:encrypt|decrypt|new)/i.test(line)) {
739
+ cryptoMiscLines.push(i + 1);
740
+ }
703
741
  }
704
742
  const uniqueCrypto = [...new Set(cryptoMiscLines)].sort((a, b) => a - b);
705
743
  if (uniqueCrypto.length > 0) {
@@ -785,5 +823,191 @@ export function analyzeSecurity(code, language) {
785
823
  });
786
824
  }
787
825
  }
826
+ ruleNum++; // advance past SEC-022
827
+ // ── SEC-023: C/C++ unsafe memory functions ─────────────────────────────
828
+ if (lang === "cpp") {
829
+ const unsafeMemLines = [];
830
+ for (let i = 0; i < lines.length; i++) {
831
+ const line = lines[i];
832
+ // strcpy, strcat, sprintf, gets, sscanf — no bounds checking
833
+ if (/\b(?:strcpy|strcat|sprintf|gets|sscanf|wcscpy|wcscat|swprintf)\s*\(/i.test(line)) {
834
+ unsafeMemLines.push(i + 1);
835
+ }
836
+ // memcpy with potentially unbounded size from user input
837
+ if (/\bmemcpy\s*\(/.test(line) && /sizeof\s*\(\s*\w+\s*\)/.test(line) === false) {
838
+ const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 2)).join("\n");
839
+ if (/strlen|input|user|request|param|argv|read/i.test(ctx)) {
840
+ unsafeMemLines.push(i + 1);
841
+ }
842
+ }
843
+ // Use-after-free: free() followed by use of same pointer
844
+ if (/\bfree\s*\(\s*(\w+)\s*\)/.test(line)) {
845
+ const match = line.match(/\bfree\s*\(\s*(\w+)\s*\)/);
846
+ if (match) {
847
+ const varName = match[1];
848
+ const after = lines.slice(i + 1, Math.min(lines.length, i + 6)).join("\n");
849
+ const useRe = new RegExp(`\\b${varName}\\b(?!\\s*=\\s*NULL|\\s*=\\s*nullptr|\\s*=\\s*0)`, "i");
850
+ if (useRe.test(after)) {
851
+ unsafeMemLines.push(i + 1);
852
+ }
853
+ }
854
+ }
855
+ }
856
+ if (unsafeMemLines.length > 0) {
857
+ findings.push({
858
+ ruleId: `${prefix}-${String(ruleNum).padStart(3, "0")}`,
859
+ severity: "critical",
860
+ title: "Unsafe memory functions without bounds checking",
861
+ description: "Functions like strcpy, gets, sprintf, and strcat perform no bounds checking and are primary sources of buffer overflow vulnerabilities. Use-after-free patterns also detected.",
862
+ lineNumbers: [...new Set(unsafeMemLines)].sort((a, b) => a - b),
863
+ recommendation: "Replace with bounds-checked alternatives: strncpy/strlcpy, snprintf, fgets, strncat. Set freed pointers to NULL. Consider using std::string in C++.",
864
+ reference: "CWE-120 / CWE-416",
865
+ suggestedFix: "strcpy(dest, src) → strncpy(dest, src, sizeof(dest)-1); gets(buf) → fgets(buf, sizeof(buf), stdin);",
866
+ confidence: 0.95,
867
+ });
868
+ }
869
+ }
870
+ ruleNum++; // advance past SEC-023
871
+ // ── SEC-024: NoSQL injection via unsanitized query objects ─────────────
872
+ {
873
+ const nosqlLines = [];
874
+ for (let i = 0; i < lines.length; i++) {
875
+ const line = lines[i];
876
+ // MongoDB-style: collection.find/deleteMany/updateMany with raw user input
877
+ if (/\.(?:find|findOne|findOneAndUpdate|findOneAndDelete|updateOne|updateMany|deleteOne|deleteMany|aggregate|countDocuments)\s*\(/i.test(line)) {
878
+ // Direct user input in the function call
879
+ if (/(?:req\.body|req\.query|req\.params|request\.body|request\.args)/i.test(line)) {
880
+ nosqlLines.push(i + 1);
881
+ }
882
+ // Indirect: check if the argument variable was assigned from user input
883
+ const match = line.match(/\.(?:find|findOne|findOneAndUpdate|findOneAndDelete|deleteMany|updateMany)\s*\(\s*(\w+)/i);
884
+ if (match && match[1]) {
885
+ const varName = match[1];
886
+ if (!/^['"`{[]/.test(varName) && !/^(?:null|undefined|true|false|\d)/.test(varName)) {
887
+ const ctx = lines.slice(Math.max(0, i - 8), i).join("\n");
888
+ const assignRe = new RegExp(`(?:const|let|var)\\s+${varName}\\s*=\\s*.*(?:req\\.body|req\\.query|req\\.params|request\\.body|request\\.args)`, "i");
889
+ if (assignRe.test(ctx)) {
890
+ nosqlLines.push(i + 1);
891
+ }
892
+ }
893
+ }
894
+ }
895
+ // MongoDB $where with string (code injection)
896
+ if (/\$where\s*:\s*['"`]/.test(line)) {
897
+ nosqlLines.push(i + 1);
898
+ }
899
+ }
900
+ if (nosqlLines.length > 0) {
901
+ findings.push({
902
+ ruleId: `${prefix}-${String(ruleNum).padStart(3, "0")}`,
903
+ severity: "critical",
904
+ title: "NoSQL injection via unsanitized query object",
905
+ description: "User input is passed directly as a query filter to NoSQL database operations. Attackers can inject operators like $gt, $ne, or $where to bypass authentication or extract data.",
906
+ lineNumbers: [...new Set(nosqlLines)].sort((a, b) => a - b),
907
+ recommendation: "Validate and sanitize query objects. Use explicit field selection instead of passing raw request body. Strip MongoDB operators ($gt, $ne, $regex, $where) from user input.",
908
+ reference: "CWE-943",
909
+ suggestedFix: "const filter = { status: req.body.status }; // whitelist fields instead of: collection.find(req.body)",
910
+ confidence: 0.9,
911
+ });
912
+ }
913
+ }
914
+ ruleNum++; // advance past SEC-024
915
+ // ── SEC-025: CORS wildcard origin with credentials ─────────────────────
916
+ {
917
+ const corsLines = [];
918
+ for (let i = 0; i < lines.length; i++) {
919
+ const line = lines[i];
920
+ // Python Flask-CORS: origins="*" + supports_credentials=True
921
+ if (/origins?\s*[:=]\s*["']\*["']/i.test(line)) {
922
+ const ctx = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 5)).join("\n");
923
+ if (/(?:supports_credentials|credentials)\s*[:=]\s*(?:True|true)/i.test(ctx)) {
924
+ corsLines.push(i + 1);
925
+ }
926
+ }
927
+ // Express cors: origin: "*" + credentials: true
928
+ if (/origin\s*:\s*["']\*["']|origin\s*:\s*true/i.test(line)) {
929
+ const ctx = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 5)).join("\n");
930
+ if (/credentials\s*:\s*true/i.test(ctx)) {
931
+ corsLines.push(i + 1);
932
+ }
933
+ }
934
+ // Raw header: Access-Control-Allow-Origin: *
935
+ if (/Access-Control-Allow-Origin['":\s]*\*/i.test(line)) {
936
+ const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 3)).join("\n");
937
+ if (/Access-Control-Allow-Credentials['":\s]*true/i.test(ctx)) {
938
+ corsLines.push(i + 1);
939
+ }
940
+ }
941
+ }
942
+ if (corsLines.length > 0) {
943
+ findings.push({
944
+ ruleId: `${prefix}-${String(ruleNum).padStart(3, "0")}`,
945
+ severity: "high",
946
+ title: "CORS wildcard origin with credentials enabled",
947
+ description: "Setting Access-Control-Allow-Origin to '*' while enabling credentials is a dangerous misconfiguration. Browsers block this combination, but misconfigurations in server handling can still leak session cookies to arbitrary origins.",
948
+ lineNumbers: corsLines,
949
+ recommendation: "Use an explicit allowlist of origins instead of '*' when credentials are required. Validate the Origin header against trusted domains.",
950
+ reference: "CWE-346 / CWE-942",
951
+ suggestedFix: "Replace origin='*' with specific allowed origins: CORS(app, origins=['https://myapp.com'], supports_credentials=True)",
952
+ confidence: 0.9,
953
+ });
954
+ }
955
+ }
956
+ ruleNum++; // advance past SEC-025
957
+ // ── SEC-026: Elixir atom exhaustion from user input ────────────────────
958
+ {
959
+ const atomLines = [];
960
+ for (let i = 0; i < lines.length; i++) {
961
+ const line = lines[i];
962
+ // String.to_atom or String.to_existing_atom from user input
963
+ if (/String\.to_atom\s*\(/i.test(line)) {
964
+ const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 2)).join("\n");
965
+ if (/(?:params|conn\.params|request|body|query|input|assigns)/i.test(ctx)) {
966
+ atomLines.push(i + 1);
967
+ }
968
+ }
969
+ }
970
+ if (atomLines.length > 0) {
971
+ findings.push({
972
+ ruleId: `${prefix}-${String(ruleNum).padStart(3, "0")}`,
973
+ severity: "high",
974
+ title: "Atom exhaustion from uncontrolled user input",
975
+ description: "Converting user input to atoms via String.to_atom/1 can exhaust the atom table (atoms are never garbage collected), leading to a denial-of-service crash of the BEAM VM.",
976
+ lineNumbers: atomLines,
977
+ recommendation: "Use String.to_existing_atom/1 instead, which only converts to atoms that already exist. Alternatively, use a whitelist of allowed values.",
978
+ reference: "CWE-400",
979
+ suggestedFix: "String.to_atom(input) → String.to_existing_atom(input) or validate: if input in ~w(index show), do: ...",
980
+ confidence: 0.95,
981
+ });
982
+ }
983
+ }
984
+ ruleNum++; // advance past SEC-026
985
+ // ── SEC-027: Dynamic code execution (loadstring, eval equivalents) ─────
986
+ {
987
+ const dynCodeLines = [];
988
+ for (let i = 0; i < lines.length; i++) {
989
+ const line = lines[i];
990
+ // Lua: loadstring / load with user input (code execution)
991
+ if (/\b(?:loadstring|load)\s*\(\s*(\w+)/i.test(line)) {
992
+ const match = line.match(/\b(?:loadstring|load)\s*\(\s*(\w+)/i);
993
+ if (match && !/^['"`]/.test(match[1])) {
994
+ dynCodeLines.push(i + 1);
995
+ }
996
+ }
997
+ }
998
+ if (dynCodeLines.length > 0) {
999
+ findings.push({
1000
+ ruleId: `${prefix}-${String(ruleNum).padStart(3, "0")}`,
1001
+ severity: "critical",
1002
+ title: "Dynamic code execution with potentially untrusted input",
1003
+ description: "Functions like loadstring (Lua) compile and execute strings as code. When called with untrusted input, attackers can execute arbitrary code on the server.",
1004
+ lineNumbers: dynCodeLines,
1005
+ recommendation: "Avoid loadstring/load with external input. Use a sandboxed environment or whitelist of allowed operations. Consider using a data-driven approach instead of code generation.",
1006
+ reference: "CWE-94",
1007
+ suggestedFix: "Replace loadstring(code) with a safe dispatch table: actions[command](args) using pre-defined functions.",
1008
+ confidence: 0.85,
1009
+ });
1010
+ }
1011
+ }
788
1012
  return findings;
789
1013
  }
@@ -0,0 +1,49 @@
1
+ import type { Finding, SuppressionResult } from "../types.js";
2
+ /**
3
+ * Metadata captured per suppression directive during parsing.
4
+ */
5
+ export interface SuppressionDirective {
6
+ /** Normalised rule ID (uppercased) or "*" */
7
+ ruleId: string;
8
+ /** Type of directive that created this suppression */
9
+ kind: "line" | "next-line" | "block" | "file";
10
+ /** 1-based line number of the suppression comment itself */
11
+ commentLine: number;
12
+ /** Optional reason text extracted from the comment */
13
+ reason?: string;
14
+ }
15
+ /**
16
+ * Parsed result of inline suppression comments in source code.
17
+ *
18
+ * Supports five directive styles:
19
+ * // judges-ignore RULE-ID → suppress on same line
20
+ * // judges-ignore-next-line RULE-ID → suppress on the next line
21
+ * // judges-ignore-block RULE-ID → suppress until matching end
22
+ * // judges-end-block → ends block suppression
23
+ * // judges-file-ignore RULE-ID → suppress across entire file
24
+ *
25
+ * All directive styles also accept # and /* comment prefixes for
26
+ * Python/YAML/CSS compatibility.
27
+ *
28
+ * An optional reason can be appended after " -- ":
29
+ * // judges-ignore SEC-001 -- legacy code, tracked in JIRA-123
30
+ */
31
+ export declare function parseInlineSuppressions(code: string): {
32
+ lineSuppressed: Map<number, SuppressionDirective[]>;
33
+ globalSuppressed: SuppressionDirective[];
34
+ };
35
+ /**
36
+ * Check whether a rule ID matches a set of suppression directives.
37
+ * Supports exact match, wildcard "*", and prefix wildcards like "AUTH-*".
38
+ */
39
+ export declare function matchesSuppression(ruleUpper: string, directives: SuppressionDirective[]): SuppressionDirective | undefined;
40
+ /**
41
+ * Apply inline suppression comments and return both filtered findings
42
+ * and a full audit trail of what was suppressed.
43
+ */
44
+ export declare function applyInlineSuppressionsWithAudit(findings: Finding[], code: string): SuppressionResult;
45
+ /**
46
+ * Filter findings based on inline suppression comments in the source code.
47
+ * Drop-in backward-compatible wrapper around `applyInlineSuppressionsWithAudit`.
48
+ */
49
+ export declare function applyInlineSuppressions(findings: Finding[], code: string): Finding[];
@@ -0,0 +1,185 @@
1
+ // ── Inline suppression comment support ──────────────────────────────────────
2
+ // Extracted from evaluators/index.ts to keep that file focused on
3
+ // tribunal orchestration and scoring.
4
+ // ────────────────────────────────────────────────────────────────────────────
5
+ /**
6
+ * Parsed result of inline suppression comments in source code.
7
+ *
8
+ * Supports five directive styles:
9
+ * // judges-ignore RULE-ID → suppress on same line
10
+ * // judges-ignore-next-line RULE-ID → suppress on the next line
11
+ * // judges-ignore-block RULE-ID → suppress until matching end
12
+ * // judges-end-block → ends block suppression
13
+ * // judges-file-ignore RULE-ID → suppress across entire file
14
+ *
15
+ * All directive styles also accept # and /* comment prefixes for
16
+ * Python/YAML/CSS compatibility.
17
+ *
18
+ * An optional reason can be appended after " -- ":
19
+ * // judges-ignore SEC-001 -- legacy code, tracked in JIRA-123
20
+ */
21
+ export function parseInlineSuppressions(code) {
22
+ const lines = code.split("\n");
23
+ const lineSuppressed = new Map();
24
+ const globalSuppressed = [];
25
+ // Active block suppressions: ruleId → { commentLine, reason }
26
+ const activeBlocks = new Map();
27
+ // Pattern: // judges-ignore[-next-line|-block] RULE-ID [, RULE-ID ...] [-- reason]
28
+ const endBlockPattern = /(?:\/\/|#|\/\*)\s*judges-end-block/i;
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const line = lines[i];
31
+ const lineNum = i + 1; // 1-indexed
32
+ // Check for block-end
33
+ if (endBlockPattern.test(line)) {
34
+ activeBlocks.clear();
35
+ }
36
+ // Apply any active block suppressions to this line
37
+ for (const [ruleId, meta] of activeBlocks) {
38
+ const arr = lineSuppressed.get(lineNum) ?? [];
39
+ arr.push({ ruleId, kind: "block", commentLine: meta.commentLine, reason: meta.reason });
40
+ lineSuppressed.set(lineNum, arr);
41
+ }
42
+ // Parse suppression directives (string-based to avoid regex redos)
43
+ const ignoreIdx = line.indexOf("judges-ignore");
44
+ if (ignoreIdx >= 0) {
45
+ const before = line.substring(0, ignoreIdx).trimEnd();
46
+ if (before.endsWith("//") || before.endsWith("#") || before.endsWith("/*")) {
47
+ let rest = line.substring(ignoreIdx + "judges-ignore".length);
48
+ let modifier;
49
+ if (rest.toLowerCase().startsWith("-next-line")) {
50
+ modifier = "next-line";
51
+ rest = rest.substring("-next-line".length);
52
+ }
53
+ else if (rest.toLowerCase().startsWith("-block")) {
54
+ modifier = "block";
55
+ rest = rest.substring("-block".length);
56
+ }
57
+ const trimmedRest = rest.trimStart();
58
+ if (trimmedRest.length < rest.length && trimmedRest.length > 0) {
59
+ let rawContent = trimmedRest;
60
+ if (rawContent.trimEnd().endsWith("*/")) {
61
+ rawContent = rawContent.replace("*/", "").trimEnd();
62
+ }
63
+ const dashSplit = rawContent.split(" -- ");
64
+ const ruleIds = dashSplit[0].split(/[, \t]+/).filter(Boolean);
65
+ const reason = dashSplit[1]?.trim() || undefined;
66
+ const kind = modifier === "next-line" ? "next-line" : modifier === "block" ? "block" : "line";
67
+ const targetLine = kind === "next-line" ? lineNum + 1 : lineNum;
68
+ for (const rawId of ruleIds) {
69
+ const ruleId = rawId === "*" ? "*" : rawId.toUpperCase();
70
+ if (kind === "block") {
71
+ // Start block suppression — applies to all subsequent lines until end-block
72
+ activeBlocks.set(ruleId, { commentLine: lineNum, reason });
73
+ }
74
+ else {
75
+ const arr = lineSuppressed.get(targetLine) ?? [];
76
+ arr.push({ ruleId, kind, commentLine: lineNum, reason });
77
+ lineSuppressed.set(targetLine, arr);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ // File-level suppression: // judges-file-ignore RULE-ID [-- reason]
84
+ const fileIgnoreIdx = line.indexOf("judges-file-ignore");
85
+ if (fileIgnoreIdx >= 0) {
86
+ const beforeFile = line.substring(0, fileIgnoreIdx).trimEnd();
87
+ if (beforeFile.endsWith("//") || beforeFile.endsWith("#") || beforeFile.endsWith("/*")) {
88
+ const fileRest = line.substring(fileIgnoreIdx + "judges-file-ignore".length);
89
+ const fileTrimmedRest = fileRest.trimStart();
90
+ if (fileTrimmedRest.length < fileRest.length && fileTrimmedRest.length > 0) {
91
+ let rawFileContent = fileTrimmedRest;
92
+ if (rawFileContent.trimEnd().endsWith("*/")) {
93
+ rawFileContent = rawFileContent.replace("*/", "").trimEnd();
94
+ }
95
+ const fileDashSplit = rawFileContent.split(" -- ");
96
+ const ruleIds = fileDashSplit[0].split(/[, \t]+/).filter(Boolean);
97
+ const reason = fileDashSplit[1]?.trim() || undefined;
98
+ for (const rawId of ruleIds) {
99
+ const ruleId = rawId === "*" ? "*" : rawId.toUpperCase();
100
+ globalSuppressed.push({ ruleId, kind: "file", commentLine: lineNum, reason });
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return { lineSuppressed, globalSuppressed };
107
+ }
108
+ /**
109
+ * Check whether a rule ID matches a set of suppression directives.
110
+ * Supports exact match, wildcard "*", and prefix wildcards like "AUTH-*".
111
+ */
112
+ export function matchesSuppression(ruleUpper, directives) {
113
+ for (const d of directives) {
114
+ if (d.ruleId === "*" || d.ruleId === ruleUpper) {
115
+ return d;
116
+ }
117
+ if (d.ruleId.endsWith("-*") && ruleUpper.startsWith(d.ruleId.slice(0, -1))) {
118
+ return d;
119
+ }
120
+ }
121
+ return undefined;
122
+ }
123
+ /**
124
+ * Apply inline suppression comments and return both filtered findings
125
+ * and a full audit trail of what was suppressed.
126
+ */
127
+ export function applyInlineSuppressionsWithAudit(findings, code) {
128
+ const { lineSuppressed, globalSuppressed } = parseInlineSuppressions(code);
129
+ if (lineSuppressed.size === 0 && globalSuppressed.length === 0) {
130
+ return { findings, suppressed: [] };
131
+ }
132
+ const kept = [];
133
+ const suppressed = [];
134
+ for (const f of findings) {
135
+ const ruleUpper = f.ruleId.toUpperCase();
136
+ // Check file-level suppression
137
+ const globalMatch = matchesSuppression(ruleUpper, globalSuppressed);
138
+ if (globalMatch) {
139
+ suppressed.push({
140
+ ruleId: f.ruleId,
141
+ severity: f.severity,
142
+ title: f.title,
143
+ kind: globalMatch.kind,
144
+ commentLine: globalMatch.commentLine,
145
+ findingLines: f.lineNumbers,
146
+ reason: globalMatch.reason,
147
+ });
148
+ continue;
149
+ }
150
+ // Check line-level suppressions
151
+ let wasLineSuppressed = false;
152
+ if (f.lineNumbers && f.lineNumbers.length > 0) {
153
+ for (const lineNum of f.lineNumbers) {
154
+ const directives = lineSuppressed.get(lineNum);
155
+ if (directives) {
156
+ const lineMatch = matchesSuppression(ruleUpper, directives);
157
+ if (lineMatch) {
158
+ suppressed.push({
159
+ ruleId: f.ruleId,
160
+ severity: f.severity,
161
+ title: f.title,
162
+ kind: lineMatch.kind,
163
+ commentLine: lineMatch.commentLine,
164
+ findingLines: f.lineNumbers,
165
+ reason: lineMatch.reason,
166
+ });
167
+ wasLineSuppressed = true;
168
+ break;
169
+ }
170
+ }
171
+ }
172
+ }
173
+ if (!wasLineSuppressed) {
174
+ kept.push(f);
175
+ }
176
+ }
177
+ return { findings: kept, suppressed };
178
+ }
179
+ /**
180
+ * Filter findings based on inline suppression comments in the source code.
181
+ * Drop-in backward-compatible wrapper around `applyInlineSuppressionsWithAudit`.
182
+ */
183
+ export function applyInlineSuppressions(findings, code) {
184
+ return applyInlineSuppressionsWithAudit(findings, code).findings;
185
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Canonical file-extension → language mapping.
3
+ *
4
+ * Single source of truth — every module that needs extension-based language
5
+ * detection should import from here instead of maintaining its own copy.
6
+ */
7
+ export declare const EXT_TO_LANG: Record<string, string>;
8
+ /** Set of all recognised source-code extensions (keys of EXT_TO_LANG). */
9
+ export declare const SUPPORTED_EXTENSIONS: Set<string>;
10
+ /**
11
+ * Detect language from a file path.
12
+ *
13
+ * Returns `undefined` when the extension is not recognised.
14
+ * Callers that need a default should coalesce: `detectLanguage(p) ?? "typescript"`.
15
+ */
16
+ export declare function detectLanguageFromPath(filePath: string): string | undefined;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Canonical file-extension → language mapping.
3
+ *
4
+ * Single source of truth — every module that needs extension-based language
5
+ * detection should import from here instead of maintaining its own copy.
6
+ */
7
+ import { extname } from "path";
8
+ // ─── Extension → Language ───────────────────────────────────────────────────
9
+ export const EXT_TO_LANG = {
10
+ ".ts": "typescript",
11
+ ".tsx": "typescript",
12
+ ".js": "javascript",
13
+ ".jsx": "javascript",
14
+ ".mjs": "javascript",
15
+ ".cjs": "javascript",
16
+ ".py": "python",
17
+ ".rs": "rust",
18
+ ".go": "go",
19
+ ".java": "java",
20
+ ".cs": "csharp",
21
+ ".rb": "ruby",
22
+ ".php": "php",
23
+ ".swift": "swift",
24
+ ".kt": "kotlin",
25
+ ".scala": "scala",
26
+ ".c": "c",
27
+ ".cc": "cpp",
28
+ ".cpp": "cpp",
29
+ ".cxx": "cpp",
30
+ ".h": "c",
31
+ ".hpp": "cpp",
32
+ ".yaml": "yaml",
33
+ ".yml": "yaml",
34
+ ".json": "json",
35
+ ".tf": "terraform",
36
+ ".hcl": "terraform",
37
+ ".dockerfile": "dockerfile",
38
+ ".sh": "bash",
39
+ ".bash": "bash",
40
+ ".ps1": "powershell",
41
+ ".psm1": "powershell",
42
+ ".dart": "dart",
43
+ ".sql": "sql",
44
+ ".bicep": "bicep",
45
+ };
46
+ /** Set of all recognised source-code extensions (keys of EXT_TO_LANG). */
47
+ export const SUPPORTED_EXTENSIONS = new Set(Object.keys(EXT_TO_LANG));
48
+ /**
49
+ * Detect language from a file path.
50
+ *
51
+ * Returns `undefined` when the extension is not recognised.
52
+ * Callers that need a default should coalesce: `detectLanguage(p) ?? "typescript"`.
53
+ */
54
+ export function detectLanguageFromPath(filePath) {
55
+ const lower = filePath.toLowerCase();
56
+ if (lower.endsWith("dockerfile") || lower.includes("dockerfile."))
57
+ return "dockerfile";
58
+ const ext = extname(lower);
59
+ return EXT_TO_LANG[ext];
60
+ }
@@ -101,8 +101,7 @@ interface WebhookResult {
101
101
  reviewPosted?: boolean;
102
102
  findingsCount?: number;
103
103
  }
104
- export declare const EXT_TO_LANG: Record<string, string>;
105
- export declare function detectLanguage(filePath: string): string | undefined;
104
+ export { EXT_TO_LANG, detectLanguageFromPath as detectLanguage } from "./ext-to-lang.js";
106
105
  export declare function generateJwt(appId: string, privateKey: string): string;
107
106
  declare function ghApi(method: string, path: string, token: string, body?: unknown): Promise<{
108
107
  status: number;
@@ -151,4 +150,3 @@ export declare function startAppServer(config: GitHubAppConfig): void;
151
150
  * `judges app serve` — Start the GitHub App webhook server.
152
151
  */
153
152
  export declare function runAppCommand(args: string[]): void;
154
- export {};
@@ -30,40 +30,8 @@ import { buildContextSnippets } from "./context/context-snippets.js";
30
30
  export let evaluateWithTribunalImpl = evaluateWithTribunal;
31
31
  export let evaluateProjectImpl = evaluateProject;
32
32
  // ─── Language Detection ─────────────────────────────────────────────────────
33
- export const EXT_TO_LANG = {
34
- ".ts": "typescript",
35
- ".tsx": "typescript",
36
- ".js": "javascript",
37
- ".jsx": "javascript",
38
- ".mjs": "javascript",
39
- ".cjs": "javascript",
40
- ".py": "python",
41
- ".rs": "rust",
42
- ".go": "go",
43
- ".java": "java",
44
- ".cs": "csharp",
45
- ".rb": "ruby",
46
- ".php": "php",
47
- ".swift": "swift",
48
- ".kt": "kotlin",
49
- ".tf": "terraform",
50
- ".hcl": "terraform",
51
- ".bicep": "bicep",
52
- ".sh": "bash",
53
- ".ps1": "powershell",
54
- ".c": "c",
55
- ".cpp": "cpp",
56
- ".h": "c",
57
- ".hpp": "cpp",
58
- ".dart": "dart",
59
- ".sql": "sql",
60
- };
61
- export function detectLanguage(filePath) {
62
- const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0] ?? "";
63
- if (filePath.toLowerCase().includes("dockerfile"))
64
- return "dockerfile";
65
- return EXT_TO_LANG[ext];
66
- }
33
+ export { EXT_TO_LANG, detectLanguageFromPath as detectLanguage } from "./ext-to-lang.js";
34
+ import { detectLanguageFromPath as detectLanguage } from "./ext-to-lang.js";
67
35
  // ─── JWT Generation (RS256, no dependencies) ───────────────────────────────
68
36
  import { sign as cryptoSign, createPrivateKey } from "crypto";
69
37
  export function generateJwt(appId, privateKey) {
package/dist/parallel.js CHANGED
@@ -11,22 +11,10 @@
11
11
  */
12
12
  import { cpus } from "os";
13
13
  import { readFileSync } from "fs";
14
- import { extname } from "path";
15
14
  // ─── Language Detection ─────────────────────────────────────────────────────
16
- const EXT_TO_LANG = {
17
- ".ts": "typescript",
18
- ".tsx": "typescript",
19
- ".js": "javascript",
20
- ".jsx": "javascript",
21
- ".py": "python",
22
- ".rs": "rust",
23
- ".go": "go",
24
- ".java": "java",
25
- ".cs": "csharp",
26
- ".cpp": "cpp",
27
- };
15
+ import { detectLanguageFromPath } from "./ext-to-lang.js";
28
16
  export function detectLanguage(filePath) {
29
- return EXT_TO_LANG[extname(filePath).toLowerCase()] || "typescript";
17
+ return detectLanguageFromPath(filePath) ?? "typescript";
30
18
  }
31
19
  // ─── Sequential (fallback) ──────────────────────────────────────────────────
32
20
  /**