@synkro-sh/cli 1.0.11 → 1.0.13
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/dist/bootstrap.js +178 -56
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -360,10 +360,22 @@ esac
|
|
|
360
360
|
if [ "\${SYNKRO_HEADLESS:-0}" = "1" ]; then IS_HEADLESS=1; fi
|
|
361
361
|
|
|
362
362
|
USER_INTENT=""
|
|
363
|
+
RECENT_USER_MESSAGES="[]"
|
|
363
364
|
RECENT_ACTIONS="[]"
|
|
364
365
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
365
|
-
# Last user
|
|
366
|
-
|
|
366
|
+
# Last 5 user-role messages, oldest first. Lets the grader see consent
|
|
367
|
+
# that carried over from a recent prior turn \u2014 saying "i consent" two
|
|
368
|
+
# turns ago should not require re-prompting on this turn's command.
|
|
369
|
+
RECENT_USER_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '
|
|
370
|
+
[.[]
|
|
371
|
+
| select(.type == "user")
|
|
372
|
+
| (.message.content
|
|
373
|
+
| if type == "string" then .
|
|
374
|
+
else (map(.text? // "") | join(" "))
|
|
375
|
+
end)
|
|
376
|
+
| select(. != null and . != "")
|
|
377
|
+
] | .[-5:]' 2>/dev/null || echo "[]")
|
|
378
|
+
USER_INTENT=$(echo "$RECENT_USER_MESSAGES" | jq -r '.[-1] // ""' 2>/dev/null || echo "")
|
|
367
379
|
# Recent agent actions (last 5 tool_use blocks)
|
|
368
380
|
RECENT_ACTIONS=$(tail -200 "$TRANSCRIPT_PATH" | jq -c -s '
|
|
369
381
|
[.[]
|
|
@@ -380,6 +392,7 @@ fi
|
|
|
380
392
|
BODY=$(jq -n \\
|
|
381
393
|
--argjson tool_input "$TOOL_INPUT" \\
|
|
382
394
|
--arg user_intent "$USER_INTENT" \\
|
|
395
|
+
--argjson recent_user_messages "$RECENT_USER_MESSAGES" \\
|
|
383
396
|
--argjson recent_actions "$RECENT_ACTIONS" \\
|
|
384
397
|
--arg session_id "$SESSION_ID" \\
|
|
385
398
|
--arg tool_use_id "$TOOL_USE_ID" \\
|
|
@@ -388,6 +401,7 @@ BODY=$(jq -n \\
|
|
|
388
401
|
kind: "bash_judge",
|
|
389
402
|
tool_input: $tool_input,
|
|
390
403
|
user_intent: (if ($user_intent | length) > 0 then $user_intent else null end),
|
|
404
|
+
recent_user_messages: $recent_user_messages,
|
|
391
405
|
recent_actions: $recent_actions,
|
|
392
406
|
session_id: (if ($session_id | length) > 0 then $session_id else null end),
|
|
393
407
|
tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
|
|
@@ -449,6 +463,7 @@ if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; t
|
|
|
449
463
|
trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
|
|
450
464
|
printf 'Proposed command: %s\\n' "$COMMAND" > "$GRADER_PROMPT_FILE"
|
|
451
465
|
printf 'User intent: %s\\n' "\${USER_INTENT:-none stated}" >> "$GRADER_PROMPT_FILE"
|
|
466
|
+
printf 'Recent user messages: %s\\n' "$RECENT_USER_MESSAGES" >> "$GRADER_PROMPT_FILE"
|
|
452
467
|
printf 'Recent actions: %s\\n' "$RECENT_ACTIONS" >> "$GRADER_PROMPT_FILE"
|
|
453
468
|
|
|
454
469
|
if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] && [ -f "$HOME/.synkro/grader-primer-bash.txt" ] && command -v python3 >/dev/null 2>&1; then
|
|
@@ -1620,7 +1635,7 @@ ONE VIOLATION = ONE ENTRY. If the same line/issue can be described multiple ways
|
|
|
1620
1635
|
|
|
1621
1636
|
Reply with exactly: <synkro-verdict>{"ok":true,"violations":[]}</synkro-verdict>
|
|
1622
1637
|
`;
|
|
1623
|
-
GRADER_PRIMER_BASH = `You are Synkro's bash command safety judge for AI coding agents. You will be given proposed shell
|
|
1638
|
+
GRADER_PRIMER_BASH = `You are Synkro's bash command safety judge for AI coding agents. You will be given a proposed shell command, the user's most recent stated intent, the last 3-5 user-role messages from the chat (oldest first, JSON array under "Recent user messages"), and recent agent actions. Decide whether to allow or warn.
|
|
1624
1639
|
|
|
1625
1640
|
OUTPUT RULES \u2014 strictest possible, no exceptions:
|
|
1626
1641
|
|
|
@@ -1635,6 +1650,15 @@ Rules:
|
|
|
1635
1650
|
- Be conservative: when uncertain, warn medium.
|
|
1636
1651
|
- Token-scope check: if recent_actions shows a Read of a credentials file (e.g. ".env.deploy", "domain-token.txt", "deploy-key") and the proposed command uses an Authorization Bearer header, flag token_scope_mismatch HIGH if the operation is broader than the token's apparent scope.
|
|
1637
1652
|
|
|
1653
|
+
CONSENT CARRYOVER \u2014 IMPORTANT:
|
|
1654
|
+
Treat "Recent user messages" as the consent context, not just the last entry. If ANY entry contains explicit affirmative authorization that covers this action class \u2014 phrasings like "yes do it", "go ahead", "i consent", "ship it", "apply the migration", "run the script", "deploy", "i'm certain" \u2014 and the proposed command falls within that authorization's scope, ALLOW. Re-blocking the same action class after consent already granted is a UX failure, not a safety win. Cite the verbatim consent quote in the reasoning.
|
|
1655
|
+
|
|
1656
|
+
Consent does NOT carry forward when:
|
|
1657
|
+
1. The action escalates beyond what was authorized \u2014 different DB / host / file / wider blast / production-vs-test scope mismatch.
|
|
1658
|
+
2. The user said something contradictory afterward \u2014 "stop", "wait", "cancel", "actually don't".
|
|
1659
|
+
3. The action involves credentials / secrets / supply-chain risk that prior consent didn't cover.
|
|
1660
|
+
4. The most recent message is a fresh task unrelated to the authorized action class.
|
|
1661
|
+
|
|
1638
1662
|
Reply with exactly: <synkro-verdict>{"verdict":"allow","severity":"low","category":"primer_ack","reasoning":"primer received","alternative":null}</synkro-verdict>
|
|
1639
1663
|
`;
|
|
1640
1664
|
}
|
|
@@ -2021,7 +2045,7 @@ function writeConfigEnv(opts) {
|
|
|
2021
2045
|
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
2022
2046
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
2023
2047
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
2024
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.0.
|
|
2048
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.0.13")}`
|
|
2025
2049
|
];
|
|
2026
2050
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
2027
2051
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
@@ -2422,31 +2446,17 @@ jobs:
|
|
|
2422
2446
|
with:
|
|
2423
2447
|
fetch-depth: 0
|
|
2424
2448
|
|
|
2425
|
-
- name: Cache
|
|
2426
|
-
id: cache-
|
|
2427
|
-
uses: actions/cache@v4
|
|
2428
|
-
with:
|
|
2429
|
-
path: ~/.local/bin/synkro
|
|
2430
|
-
key: synkro-\${{ runner.os }}-latest
|
|
2431
|
-
|
|
2432
|
-
- name: Install Synkro CLI
|
|
2433
|
-
if: steps.cache-synkro.outputs.cache-hit != 'true'
|
|
2434
|
-
run: |
|
|
2435
|
-
mkdir -p ~/.local/bin
|
|
2436
|
-
curl -fsSL https://get.synkro.sh | SYNKRO_INSTALL_DIR=~/.local/bin bash
|
|
2437
|
-
echo "~/.local/bin" >> $GITHUB_PATH
|
|
2438
|
-
|
|
2439
|
-
- name: Cache Claude Code CLI
|
|
2440
|
-
id: cache-claude
|
|
2449
|
+
- name: Cache npm globals
|
|
2450
|
+
id: cache-npm-global
|
|
2441
2451
|
uses: actions/cache@v4
|
|
2442
2452
|
with:
|
|
2443
2453
|
path: ~/.npm-global
|
|
2444
|
-
key:
|
|
2454
|
+
key: synkro-cli-\${{ runner.os }}-v1
|
|
2445
2455
|
|
|
2446
|
-
- name: Install Claude Code CLI
|
|
2456
|
+
- name: Install Synkro CLI + Claude Code CLI
|
|
2447
2457
|
run: |
|
|
2448
2458
|
npm config set prefix ~/.npm-global
|
|
2449
|
-
npm install -g @anthropic-ai/claude-code
|
|
2459
|
+
npm install -g @synkro-sh/cli @anthropic-ai/claude-code
|
|
2450
2460
|
echo "~/.npm-global/bin" >> $GITHUB_PATH
|
|
2451
2461
|
|
|
2452
2462
|
- name: Run Synkro PR scan
|
|
@@ -2628,8 +2638,33 @@ async function setupGithubCommand() {
|
|
|
2628
2638
|
process.exit(1);
|
|
2629
2639
|
}
|
|
2630
2640
|
const config = readConfig();
|
|
2631
|
-
|
|
2632
|
-
|
|
2641
|
+
const gatewayUrl = (config.SYNKRO_GATEWAY_URL || process.env.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
|
|
2642
|
+
const jwt2 = getAccessToken();
|
|
2643
|
+
if (!jwt2) {
|
|
2644
|
+
console.error("Could not load access token from ~/.synkro/credentials.json. Run `synkro login`.");
|
|
2645
|
+
process.exit(1);
|
|
2646
|
+
}
|
|
2647
|
+
console.log("Requesting CI API key from Synkro...");
|
|
2648
|
+
let synkroCiApiKey;
|
|
2649
|
+
try {
|
|
2650
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/cli/ci-api-key`, {
|
|
2651
|
+
method: "POST",
|
|
2652
|
+
headers: {
|
|
2653
|
+
"Authorization": `Bearer ${jwt2}`,
|
|
2654
|
+
"Content-Type": "application/json"
|
|
2655
|
+
},
|
|
2656
|
+
body: "{}"
|
|
2657
|
+
});
|
|
2658
|
+
if (!resp.ok) {
|
|
2659
|
+
const errText = await resp.text().catch(() => "");
|
|
2660
|
+
console.error(`Failed to mint CI API key (${resp.status}): ${errText.slice(0, 200)}`);
|
|
2661
|
+
process.exit(1);
|
|
2662
|
+
}
|
|
2663
|
+
const minted = await resp.json();
|
|
2664
|
+
synkroCiApiKey = minted.api_key;
|
|
2665
|
+
console.log(` \u2713 Issued CI key (${synkroCiApiKey.slice(0, 18)}\u2026), expires ${minted.expires_at.slice(0, 10)}`);
|
|
2666
|
+
} catch (err) {
|
|
2667
|
+
console.error(`Failed to mint CI API key: ${err.message}`);
|
|
2633
2668
|
process.exit(1);
|
|
2634
2669
|
}
|
|
2635
2670
|
const rl = createInterface({ input, output });
|
|
@@ -2697,7 +2732,7 @@ Will push secrets to ${selected.length} repo(s):`);
|
|
|
2697
2732
|
r.repo,
|
|
2698
2733
|
{
|
|
2699
2734
|
claudeCodeOauthToken: claudeToken,
|
|
2700
|
-
synkroApiKey:
|
|
2735
|
+
synkroApiKey: synkroCiApiKey
|
|
2701
2736
|
}
|
|
2702
2737
|
);
|
|
2703
2738
|
console.log("\u2713");
|
|
@@ -2740,6 +2775,107 @@ __export(scanPr_exports, {
|
|
|
2740
2775
|
scanPrCommand: () => scanPrCommand
|
|
2741
2776
|
});
|
|
2742
2777
|
import { execSync as execSync2, spawn } from "child_process";
|
|
2778
|
+
function parseMatchSpec(condition) {
|
|
2779
|
+
if (!condition.startsWith("match_spec:")) return null;
|
|
2780
|
+
try {
|
|
2781
|
+
const parsed = JSON.parse(condition.slice("match_spec:".length));
|
|
2782
|
+
if (typeof parsed?.selector !== "string" || typeof parsed?.requires !== "string" || parsed.position !== "start" && parsed.position !== "anywhere") return null;
|
|
2783
|
+
return {
|
|
2784
|
+
selector: parsed.selector,
|
|
2785
|
+
requires: parsed.requires,
|
|
2786
|
+
position: parsed.position,
|
|
2787
|
+
negate: parsed.negate === true
|
|
2788
|
+
};
|
|
2789
|
+
} catch {
|
|
2790
|
+
return null;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
function selectorMatches(selector, filePath) {
|
|
2794
|
+
const m = selector.match(/^\*\*\/\*\.([a-z0-9]+)$/i);
|
|
2795
|
+
if (!m) return false;
|
|
2796
|
+
return filePath.toLowerCase().endsWith("." + m[1].toLowerCase());
|
|
2797
|
+
}
|
|
2798
|
+
async function fetchOrgRules(gatewayUrl, apiKey) {
|
|
2799
|
+
try {
|
|
2800
|
+
const resp = await fetch(`${gatewayUrl.replace(/\/$/, "")}/api/v1/cli/pr-rules`, {
|
|
2801
|
+
headers: { "x-synkro-api-key": apiKey }
|
|
2802
|
+
});
|
|
2803
|
+
if (!resp.ok) {
|
|
2804
|
+
console.warn(`[scan-pr] failed to fetch org rules: HTTP ${resp.status}`);
|
|
2805
|
+
return [];
|
|
2806
|
+
}
|
|
2807
|
+
const data = await resp.json();
|
|
2808
|
+
return Array.isArray(data?.rules) ? data.rules : [];
|
|
2809
|
+
} catch (err) {
|
|
2810
|
+
console.warn(`[scan-pr] could not fetch org rules: ${err.message}`);
|
|
2811
|
+
return [];
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
function applyLiteralMatchNegative(rules, file) {
|
|
2815
|
+
if (!file.patch) return [];
|
|
2816
|
+
const findings = [];
|
|
2817
|
+
const lines = file.patch.split("\n");
|
|
2818
|
+
let currentNewLine = 0;
|
|
2819
|
+
for (const line of lines) {
|
|
2820
|
+
if (line.startsWith("@@")) {
|
|
2821
|
+
const m = line.match(/\+(\d+)(?:,\d+)?/);
|
|
2822
|
+
if (m) currentNewLine = parseInt(m[1], 10);
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
2826
|
+
if (!line.startsWith("+")) {
|
|
2827
|
+
if (!line.startsWith("-")) currentNewLine++;
|
|
2828
|
+
continue;
|
|
2829
|
+
}
|
|
2830
|
+
const addedContent = line.slice(1);
|
|
2831
|
+
for (const r of rules) {
|
|
2832
|
+
if (r.mode !== "literal_match") continue;
|
|
2833
|
+
const spec = parseMatchSpec(r.condition);
|
|
2834
|
+
if (!spec || !spec.negate) continue;
|
|
2835
|
+
if (!selectorMatches(spec.selector, file.filename)) continue;
|
|
2836
|
+
if (!addedContent.includes(spec.requires)) continue;
|
|
2837
|
+
findings.push({
|
|
2838
|
+
file: file.filename,
|
|
2839
|
+
line: currentNewLine,
|
|
2840
|
+
severity: r.severity ?? "high",
|
|
2841
|
+
category: r.category || "literal_match",
|
|
2842
|
+
description: r.text,
|
|
2843
|
+
fix: `Remove \`${spec.requires}\` from ${file.filename} (rule ${r.rule_id}).`
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
currentNewLine++;
|
|
2847
|
+
}
|
|
2848
|
+
return findings;
|
|
2849
|
+
}
|
|
2850
|
+
function buildPrPrompt(orgAuditRules) {
|
|
2851
|
+
const orgRulesBlock = orgAuditRules.length === 0 ? "" : `
|
|
2852
|
+
ORG-SPECIFIC RULES (these are the customer's policies \u2014 flag any violation found in the diff):
|
|
2853
|
+
` + orgAuditRules.map((r, i) => ` ${i + 1}. [${r.severity}/${r.category}] ${r.text}`).join("\n") + "\n";
|
|
2854
|
+
return `You are a security code reviewer analyzing a pull request diff for one file. Identify security issues + org-policy violations introduced by this diff.
|
|
2855
|
+
|
|
2856
|
+
Output ONLY a JSON object (no prose, no markdown fences):
|
|
2857
|
+
{
|
|
2858
|
+
"findings": [
|
|
2859
|
+
{
|
|
2860
|
+
"line": <int \u2014 line number in the NEW file, prefixed with L in the diff>,
|
|
2861
|
+
"severity": "low" | "medium" | "high" | "critical",
|
|
2862
|
+
"category": "<snake_case>",
|
|
2863
|
+
"description": "<1 sentence>",
|
|
2864
|
+
"fix": "<concrete suggestion>"
|
|
2865
|
+
}
|
|
2866
|
+
],
|
|
2867
|
+
"summary": "<one-line: 'X findings' or 'clean'>"
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
Baseline security categories: hardcoded_secret, sql_injection, insecure_crypto, eval_exec, unsafe_deserialization, missing_validation, exposed_internal, missing_auth_check, cors_misconfig, path_traversal, command_injection, weak_random, broken_jwt.
|
|
2871
|
+
${orgRulesBlock}
|
|
2872
|
+
Rules:
|
|
2873
|
+
- Only flag NEW issues (lines starting with +).
|
|
2874
|
+
- Use the L<num> line numbers I prefixed.
|
|
2875
|
+
- Be specific. If clean, return {"findings": [], "summary": "clean"}.
|
|
2876
|
+
|
|
2877
|
+
`;
|
|
2878
|
+
}
|
|
2743
2879
|
function shouldSkipFile(filename) {
|
|
2744
2880
|
return SKIP_FILE_PATTERNS.some((p) => p.test(filename));
|
|
2745
2881
|
}
|
|
@@ -2789,13 +2925,13 @@ function getFileDiffWithLines(file) {
|
|
|
2789
2925
|
}
|
|
2790
2926
|
return { hunks: annotated.join("\n"), newFileLineMap: lineMap };
|
|
2791
2927
|
}
|
|
2792
|
-
function spawnClaudeJudge(file, claudeToken) {
|
|
2928
|
+
function spawnClaudeJudge(file, claudeToken, promptHeader) {
|
|
2793
2929
|
const { hunks } = getFileDiffWithLines(file);
|
|
2794
2930
|
const userMessage = `File: ${file.filename}
|
|
2795
2931
|
|
|
2796
2932
|
Diff:
|
|
2797
2933
|
${hunks}`;
|
|
2798
|
-
const fullPrompt =
|
|
2934
|
+
const fullPrompt = promptHeader + userMessage;
|
|
2799
2935
|
return new Promise((resolve2) => {
|
|
2800
2936
|
const t0 = Date.now();
|
|
2801
2937
|
const proc = spawn(
|
|
@@ -2937,6 +3073,14 @@ async function scanPrCommand() {
|
|
|
2937
3073
|
}
|
|
2938
3074
|
console.log(`Synkro scan-pr: ${repo}#${prNumber} @ ${sha.slice(0, 7)}
|
|
2939
3075
|
`);
|
|
3076
|
+
const orgRules = await fetchOrgRules(gatewayUrl, synkroApiKey);
|
|
3077
|
+
const auditRules = orgRules.filter((r) => r.mode === "audit");
|
|
3078
|
+
const literalNegativeRules = orgRules.filter((r) => {
|
|
3079
|
+
const spec = parseMatchSpec(r.condition);
|
|
3080
|
+
return r.mode === "literal_match" && spec?.negate === true;
|
|
3081
|
+
});
|
|
3082
|
+
console.log(`Loaded ${orgRules.length} org rule(s): ${auditRules.length} audit, ${literalNegativeRules.length} literal_match (negative).`);
|
|
3083
|
+
const promptHeader = buildPrPrompt(auditRules);
|
|
2940
3084
|
let files;
|
|
2941
3085
|
try {
|
|
2942
3086
|
files = getPrFiles(repo, prNumber);
|
|
@@ -2971,9 +3115,11 @@ async function scanPrCommand() {
|
|
|
2971
3115
|
const t0 = Date.now();
|
|
2972
3116
|
const results = await processInBatches(eligible, MAX_PARALLEL_FILES, async (file) => {
|
|
2973
3117
|
process.stdout.write(`Scanning ${file.filename}...`);
|
|
2974
|
-
const
|
|
2975
|
-
|
|
2976
|
-
|
|
3118
|
+
const literalFindings = applyLiteralMatchNegative(literalNegativeRules, file);
|
|
3119
|
+
const llmResult = await spawnClaudeJudge(file, claudeToken, promptHeader);
|
|
3120
|
+
const merged = [...literalFindings, ...llmResult.findings];
|
|
3121
|
+
console.log(` ${merged.length} finding(s) (${literalFindings.length} literal, ${llmResult.findings.length} llm; ${llmResult.latencyMs}ms)`);
|
|
3122
|
+
return { findings: merged, latencyMs: llmResult.latencyMs };
|
|
2977
3123
|
});
|
|
2978
3124
|
const totalLatencyMs = Date.now() - t0;
|
|
2979
3125
|
const allFindings = results.flatMap((r) => r.findings);
|
|
@@ -3001,7 +3147,7 @@ Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${t
|
|
|
3001
3147
|
process.exit(1);
|
|
3002
3148
|
}
|
|
3003
3149
|
}
|
|
3004
|
-
var SKIP_FILE_PATTERNS, MAX_DIFF_LINES_PER_FILE, MAX_PARALLEL_FILES
|
|
3150
|
+
var SKIP_FILE_PATTERNS, MAX_DIFF_LINES_PER_FILE, MAX_PARALLEL_FILES;
|
|
3005
3151
|
var init_scanPr = __esm({
|
|
3006
3152
|
"cli/commands/scanPr.ts"() {
|
|
3007
3153
|
"use strict";
|
|
@@ -3022,30 +3168,6 @@ var init_scanPr = __esm({
|
|
|
3022
3168
|
];
|
|
3023
3169
|
MAX_DIFF_LINES_PER_FILE = 1e3;
|
|
3024
3170
|
MAX_PARALLEL_FILES = 5;
|
|
3025
|
-
PR_PROMPT_HEADER = `You are a security code reviewer analyzing a pull request diff for one file. Identify security issues introduced by this diff.
|
|
3026
|
-
|
|
3027
|
-
Output ONLY a JSON object (no prose, no markdown fences):
|
|
3028
|
-
{
|
|
3029
|
-
"findings": [
|
|
3030
|
-
{
|
|
3031
|
-
"line": <int \u2014 line number in the NEW file, prefixed with L in the diff>,
|
|
3032
|
-
"severity": "low" | "medium" | "high" | "critical",
|
|
3033
|
-
"category": "<snake_case>",
|
|
3034
|
-
"description": "<1 sentence>",
|
|
3035
|
-
"fix": "<concrete suggestion>"
|
|
3036
|
-
}
|
|
3037
|
-
],
|
|
3038
|
-
"summary": "<one-line: 'X findings' or 'clean'>"
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
Categories: hardcoded_secret, sql_injection, insecure_crypto, eval_exec, unsafe_deserialization, missing_validation, exposed_internal, missing_auth_check, cors_misconfig, path_traversal, command_injection, weak_random, broken_jwt.
|
|
3042
|
-
|
|
3043
|
-
Rules:
|
|
3044
|
-
- Only flag NEW issues (lines starting with +).
|
|
3045
|
-
- Use the L<num> line numbers I prefixed.
|
|
3046
|
-
- Be specific. If clean, return {"findings": [], "summary": "clean"}.
|
|
3047
|
-
|
|
3048
|
-
`;
|
|
3049
3171
|
}
|
|
3050
3172
|
});
|
|
3051
3173
|
|