@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.
- package/README.md +1 -1
- package/dist/api.d.ts +2 -1
- package/dist/api.js +3 -1
- package/dist/cli-dispatch.d.ts +7 -0
- package/dist/cli-dispatch.js +654 -0
- package/dist/cli-formatters.d.ts +6 -0
- package/dist/cli-formatters.js +186 -0
- package/dist/cli.js +69 -4159
- package/dist/commands/baseline.js +2 -42
- package/dist/commands/coverage.js +3 -39
- package/dist/commands/diff.js +2 -38
- package/dist/commands/fix-pr.js +2 -23
- package/dist/commands/fix.js +3 -27
- package/dist/commands/llm-benchmark.d.ts +7 -0
- package/dist/commands/llm-benchmark.js +27 -1
- package/dist/commands/quality-gate.js +1 -12
- package/dist/commands/review-parallel.js +1 -19
- package/dist/commands/review.js +2 -33
- package/dist/commands/rule-test.js +1 -15
- package/dist/commands/tune.js +2 -29
- package/dist/commands/watch.js +3 -42
- package/dist/config.js +1 -1
- package/dist/evaluators/hallucination-detection.js +343 -0
- package/dist/evaluators/index.d.ts +2 -11
- package/dist/evaluators/index.js +3 -181
- package/dist/evaluators/security.js +226 -2
- package/dist/evaluators/suppressions.d.ts +49 -0
- package/dist/evaluators/suppressions.js +185 -0
- package/dist/ext-to-lang.d.ts +16 -0
- package/dist/ext-to-lang.js +60 -0
- package/dist/github-app.d.ts +1 -3
- package/dist/github-app.js +2 -34
- package/dist/parallel.js +2 -14
- package/dist/probabilistic/llm-response-validator.js +1 -1
- package/dist/reports/public-repo-report.js +9 -1
- package/dist/skill-loader.js +9 -6
- package/dist/tools/register-evaluation.js +2 -29
- package/package.json +1 -1
- package/server.json +2 -2
- 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
|
+
}
|
package/dist/github-app.d.ts
CHANGED
|
@@ -101,8 +101,7 @@ interface WebhookResult {
|
|
|
101
101
|
reviewPosted?: boolean;
|
|
102
102
|
findingsCount?: number;
|
|
103
103
|
}
|
|
104
|
-
export
|
|
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 {};
|
package/dist/github-app.js
CHANGED
|
@@ -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
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
17
|
+
return detectLanguageFromPath(filePath) ?? "typescript";
|
|
30
18
|
}
|
|
31
19
|
// ─── Sequential (fallback) ──────────────────────────────────────────────────
|
|
32
20
|
/**
|