@sentry/warden 0.35.0 → 0.36.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/dist/cli/args.d.ts +12 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +14 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +16 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.d.ts +4 -2
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +34 -13
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/output/jsonl-schema-gen.d.ts.map +1 -1
- package/dist/cli/output/jsonl-schema-gen.js +3 -1
- package/dist/cli/output/jsonl-schema-gen.js.map +1 -1
- package/dist/cli/output/jsonl.d.ts +54 -0
- package/dist/cli/output/jsonl.d.ts.map +1 -1
- package/dist/cli/output/jsonl.js +6 -1
- package/dist/cli/output/jsonl.js.map +1 -1
- package/dist/cli/output/tasks.d.ts.map +1 -1
- package/dist/cli/output/tasks.js +7 -0
- package/dist/cli/output/tasks.js.map +1 -1
- package/dist/config/loader.d.ts +2 -2
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +3 -3
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +5 -5
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +3 -3
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/sdk/analyze.d.ts.map +1 -1
- package/dist/sdk/analyze.js +139 -10
- package/dist/sdk/analyze.js.map +1 -1
- package/dist/sdk/auth.d.ts +5 -4
- package/dist/sdk/auth.d.ts.map +1 -1
- package/dist/sdk/auth.js +9 -7
- package/dist/sdk/auth.js.map +1 -1
- package/dist/sdk/circuit-breaker.js +3 -3
- package/dist/sdk/circuit-breaker.js.map +1 -1
- package/dist/sdk/errors.d.ts +8 -0
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/errors.js +44 -1
- package/dist/sdk/errors.js.map +1 -1
- package/dist/sdk/haiku.d.ts.map +1 -1
- package/dist/sdk/haiku.js +21 -12
- package/dist/sdk/haiku.js.map +1 -1
- package/dist/sdk/otel.d.ts +15 -0
- package/dist/sdk/otel.d.ts.map +1 -1
- package/dist/sdk/otel.js +50 -6
- package/dist/sdk/otel.js.map +1 -1
- package/dist/sdk/post-process.d.ts +2 -2
- package/dist/sdk/post-process.d.ts.map +1 -1
- package/dist/sdk/post-process.js +1 -1
- package/dist/sdk/post-process.js.map +1 -1
- package/dist/sdk/runtimes/claude.d.ts.map +1 -1
- package/dist/sdk/runtimes/claude.js +45 -35
- package/dist/sdk/runtimes/claude.js.map +1 -1
- package/dist/sdk/runtimes/pi.d.ts.map +1 -1
- package/dist/sdk/runtimes/pi.js +90 -6
- package/dist/sdk/runtimes/pi.js.map +1 -1
- package/dist/sdk/runtimes/types.d.ts +2 -2
- package/dist/sdk/runtimes/types.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +12 -4
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/verify.d.ts +2 -2
- package/dist/sdk/verify.d.ts.map +1 -1
- package/dist/sdk/verify.js +1 -1
- package/dist/sdk/verify.js.map +1 -1
- package/dist/sentry-trace.d.ts +29 -0
- package/dist/sentry-trace.d.ts.map +1 -0
- package/dist/sentry-trace.js +154 -0
- package/dist/sentry-trace.js.map +1 -0
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +56 -0
- package/dist/sentry.js.map +1 -1
- package/dist/types/index.d.ts +72 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/warden/SKILL.md +0 -76
- package/skills/warden/SOURCES.md +0 -39
- package/skills/warden/SPEC.md +0 -112
- package/skills/warden/references/cli-reference.md +0 -149
- package/skills/warden/references/config-schema.md +0 -141
- package/skills/warden/references/configuration.md +0 -143
- package/skills/warden/references/creating-skills.md +0 -87
- package/skills/warden-sweep/SKILL.md +0 -78
- package/skills/warden-sweep/SOURCES.md +0 -43
- package/skills/warden-sweep/SPEC.md +0 -126
- package/skills/warden-sweep/references/issue-phase.md +0 -24
- package/skills/warden-sweep/references/organize-phase.md +0 -31
- package/skills/warden-sweep/references/patch-phase.md +0 -142
- package/skills/warden-sweep/references/patch-prompt.md +0 -72
- package/skills/warden-sweep/references/resume-and-artifacts.md +0 -51
- package/skills/warden-sweep/references/scan-phase.md +0 -46
- package/skills/warden-sweep/references/script-interfaces.md +0 -127
- package/skills/warden-sweep/references/verify-phase.md +0 -69
- package/skills/warden-sweep/references/verify-prompt.md +0 -25
- package/skills/warden-sweep/scripts/_utils.py +0 -99
- package/skills/warden-sweep/scripts/create_issue.py +0 -189
- package/skills/warden-sweep/scripts/extract_findings.py +0 -219
- package/skills/warden-sweep/scripts/find_reviewers.py +0 -114
- package/skills/warden-sweep/scripts/generate_report.py +0 -266
- package/skills/warden-sweep/scripts/index_prs.py +0 -187
- package/skills/warden-sweep/scripts/organize.py +0 -422
- package/skills/warden-sweep/scripts/scan.py +0 -584
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.9"
|
|
4
|
-
# ///
|
|
5
|
-
"""
|
|
6
|
-
Extract individual findings from warden JSONL log files.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python extract_findings.py <log-path-or-directory> -o <output.jsonl>
|
|
10
|
-
python extract_findings.py .warden/logs/ --scan-index data/scan-index.jsonl -o findings.jsonl
|
|
11
|
-
|
|
12
|
-
Reads warden JSONL logs (one skill record per line, summary as last line),
|
|
13
|
-
extracts each finding as a standalone record with a stable ID, and writes
|
|
14
|
-
one finding per line to the output file.
|
|
15
|
-
|
|
16
|
-
Finding ID format: <skill>-<sha256(title+path+line)[:8]>
|
|
17
|
-
"""
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
import argparse
|
|
21
|
-
import hashlib
|
|
22
|
-
import json
|
|
23
|
-
import os
|
|
24
|
-
import sys
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
from typing import Any
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def generate_finding_id(skill: str, title: str, path: str, line: int | None) -> str:
|
|
30
|
-
"""Generate a stable, deterministic finding ID."""
|
|
31
|
-
raw = f"{title}:{path}:{line or 0}"
|
|
32
|
-
digest = hashlib.sha256(raw.encode()).hexdigest()[:8]
|
|
33
|
-
# Sanitize skill name for use in ID
|
|
34
|
-
safe_skill = skill.replace("/", "-").replace(" ", "-").lower()
|
|
35
|
-
return f"{safe_skill}-{digest}"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def parse_jsonl_log(log_path: str) -> list[dict[str, Any]]:
|
|
39
|
-
"""Parse a warden JSONL log file and extract individual findings.
|
|
40
|
-
|
|
41
|
-
Each non-summary line has the shape:
|
|
42
|
-
{
|
|
43
|
-
"run": {...},
|
|
44
|
-
"skill": "...",
|
|
45
|
-
"findings": [{...}, ...],
|
|
46
|
-
...
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
The last line is a summary record with "type": "summary" which we skip.
|
|
50
|
-
"""
|
|
51
|
-
findings = []
|
|
52
|
-
try:
|
|
53
|
-
with open(log_path) as f:
|
|
54
|
-
for line in f:
|
|
55
|
-
line = line.strip()
|
|
56
|
-
if not line:
|
|
57
|
-
continue
|
|
58
|
-
try:
|
|
59
|
-
record = json.loads(line)
|
|
60
|
-
except json.JSONDecodeError:
|
|
61
|
-
continue
|
|
62
|
-
|
|
63
|
-
# Skip summary records
|
|
64
|
-
if record.get("type") == "summary":
|
|
65
|
-
continue
|
|
66
|
-
|
|
67
|
-
skill = record.get("skill", "unknown")
|
|
68
|
-
run_meta = record.get("run", {})
|
|
69
|
-
record_findings = record.get("findings", [])
|
|
70
|
-
|
|
71
|
-
for finding in record_findings:
|
|
72
|
-
location = finding.get("location", {})
|
|
73
|
-
file_path = location.get("path", "")
|
|
74
|
-
start_line = location.get("startLine")
|
|
75
|
-
end_line = location.get("endLine")
|
|
76
|
-
|
|
77
|
-
finding_id = generate_finding_id(
|
|
78
|
-
skill=skill,
|
|
79
|
-
title=finding.get("title", ""),
|
|
80
|
-
path=file_path,
|
|
81
|
-
line=start_line,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
normalized = {
|
|
85
|
-
"findingId": finding_id,
|
|
86
|
-
"file": file_path,
|
|
87
|
-
"skill": skill,
|
|
88
|
-
"severity": finding.get("severity", "info"),
|
|
89
|
-
"confidence": finding.get("confidence"),
|
|
90
|
-
"title": finding.get("title", ""),
|
|
91
|
-
"description": finding.get("description", ""),
|
|
92
|
-
"verification": finding.get("verification"),
|
|
93
|
-
"location": {
|
|
94
|
-
"path": file_path,
|
|
95
|
-
"startLine": start_line,
|
|
96
|
-
"endLine": end_line,
|
|
97
|
-
},
|
|
98
|
-
"suggestedFix": finding.get("suggestedFix"),
|
|
99
|
-
"logPath": log_path,
|
|
100
|
-
"runId": run_meta.get("runId", ""),
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
findings.append(normalized)
|
|
104
|
-
|
|
105
|
-
except (OSError, IOError) as e:
|
|
106
|
-
print(f"Error reading {log_path}: {e}", file=sys.stderr)
|
|
107
|
-
|
|
108
|
-
return findings
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def collect_log_paths(source: str, scan_index: str | None = None) -> list[str]:
|
|
112
|
-
"""Collect log file paths from a directory or scan index."""
|
|
113
|
-
paths: list[str] = []
|
|
114
|
-
|
|
115
|
-
if scan_index and os.path.exists(scan_index):
|
|
116
|
-
# Read log paths from scan-index.jsonl
|
|
117
|
-
seen = set()
|
|
118
|
-
total_entries = 0
|
|
119
|
-
missing = 0
|
|
120
|
-
with open(scan_index) as f:
|
|
121
|
-
for line in f:
|
|
122
|
-
line = line.strip()
|
|
123
|
-
if not line:
|
|
124
|
-
continue
|
|
125
|
-
try:
|
|
126
|
-
entry = json.loads(line)
|
|
127
|
-
except json.JSONDecodeError:
|
|
128
|
-
continue
|
|
129
|
-
if entry.get("status") != "complete":
|
|
130
|
-
continue
|
|
131
|
-
total_entries += 1
|
|
132
|
-
log_path = entry.get("logPath", "")
|
|
133
|
-
if log_path and log_path not in seen:
|
|
134
|
-
seen.add(log_path)
|
|
135
|
-
if os.path.isfile(log_path):
|
|
136
|
-
paths.append(log_path)
|
|
137
|
-
else:
|
|
138
|
-
missing += 1
|
|
139
|
-
if missing > 0:
|
|
140
|
-
print(
|
|
141
|
-
f"Warning: {missing} log path(s) from scan-index not found on disk",
|
|
142
|
-
file=sys.stderr,
|
|
143
|
-
)
|
|
144
|
-
# Only use scan-index results if we actually found logs;
|
|
145
|
-
# fall through to source directory otherwise
|
|
146
|
-
if paths:
|
|
147
|
-
return paths
|
|
148
|
-
if total_entries > 0:
|
|
149
|
-
print(
|
|
150
|
-
"Warning: scan-index had entries but no valid log paths; "
|
|
151
|
-
"falling back to source directory",
|
|
152
|
-
file=sys.stderr,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
source_path = Path(source)
|
|
156
|
-
if source_path.is_file():
|
|
157
|
-
return [str(source_path)]
|
|
158
|
-
|
|
159
|
-
if source_path.is_dir():
|
|
160
|
-
for f in sorted(source_path.glob("*.jsonl")):
|
|
161
|
-
paths.append(str(f))
|
|
162
|
-
return paths
|
|
163
|
-
|
|
164
|
-
print(f"Source not found: {source}", file=sys.stderr)
|
|
165
|
-
return paths
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def main():
|
|
169
|
-
parser = argparse.ArgumentParser(
|
|
170
|
-
description="Extract findings from warden JSONL logs"
|
|
171
|
-
)
|
|
172
|
-
parser.add_argument(
|
|
173
|
-
"source",
|
|
174
|
-
help="Path to a JSONL log file or directory of log files",
|
|
175
|
-
)
|
|
176
|
-
parser.add_argument(
|
|
177
|
-
"-o", "--output",
|
|
178
|
-
required=True,
|
|
179
|
-
help="Output path for normalized findings JSONL",
|
|
180
|
-
)
|
|
181
|
-
parser.add_argument(
|
|
182
|
-
"--scan-index",
|
|
183
|
-
help="Path to scan-index.jsonl (uses log paths from completed scans)",
|
|
184
|
-
)
|
|
185
|
-
args = parser.parse_args()
|
|
186
|
-
|
|
187
|
-
log_paths = collect_log_paths(args.source, args.scan_index)
|
|
188
|
-
if not log_paths:
|
|
189
|
-
print("No log files found.", file=sys.stderr)
|
|
190
|
-
sys.exit(1)
|
|
191
|
-
|
|
192
|
-
all_findings: list[dict[str, Any]] = []
|
|
193
|
-
seen_ids: set[str] = set()
|
|
194
|
-
|
|
195
|
-
for log_path in log_paths:
|
|
196
|
-
findings = parse_jsonl_log(log_path)
|
|
197
|
-
for f in findings:
|
|
198
|
-
fid = f["findingId"]
|
|
199
|
-
if fid not in seen_ids:
|
|
200
|
-
seen_ids.add(fid)
|
|
201
|
-
all_findings.append(f)
|
|
202
|
-
|
|
203
|
-
# Write output
|
|
204
|
-
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
|
|
205
|
-
with open(args.output, "w") as out:
|
|
206
|
-
for finding in all_findings:
|
|
207
|
-
out.write(json.dumps(finding) + "\n")
|
|
208
|
-
|
|
209
|
-
print(
|
|
210
|
-
json.dumps({
|
|
211
|
-
"logsProcessed": len(log_paths),
|
|
212
|
-
"findingsExtracted": len(all_findings),
|
|
213
|
-
"outputPath": args.output,
|
|
214
|
-
})
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if __name__ == "__main__":
|
|
219
|
-
main()
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.9"
|
|
4
|
-
# ///
|
|
5
|
-
"""
|
|
6
|
-
Find top git contributors for a file to use as PR reviewers.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python find_reviewers.py <file-path>
|
|
10
|
-
python find_reviewers.py src/foo.ts
|
|
11
|
-
|
|
12
|
-
Output: JSON to stdout with GitHub usernames of top 2 contributors
|
|
13
|
-
from the last 12 months.
|
|
14
|
-
|
|
15
|
-
{"reviewers": ["user1", "user2"]}
|
|
16
|
-
|
|
17
|
-
If no contributors found or mapping fails, returns empty list.
|
|
18
|
-
"""
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import argparse
|
|
22
|
-
import json
|
|
23
|
-
import os
|
|
24
|
-
import sys
|
|
25
|
-
|
|
26
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
27
|
-
from _utils import run_cmd_stdout as run_cmd # noqa: E402
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def get_top_authors(file_path: str, count: int = 2) -> list[str]:
|
|
31
|
-
"""Get top N author emails for a file from git log (last 12 months)."""
|
|
32
|
-
output = run_cmd([
|
|
33
|
-
"git", "log",
|
|
34
|
-
"--format=%ae",
|
|
35
|
-
"--since=12 months ago",
|
|
36
|
-
"--", file_path,
|
|
37
|
-
])
|
|
38
|
-
|
|
39
|
-
if not output:
|
|
40
|
-
return []
|
|
41
|
-
|
|
42
|
-
# Count occurrences of each email
|
|
43
|
-
email_counts: dict[str, int] = {}
|
|
44
|
-
for email in output.splitlines():
|
|
45
|
-
email = email.strip()
|
|
46
|
-
if email:
|
|
47
|
-
email_counts[email] = email_counts.get(email, 0) + 1
|
|
48
|
-
|
|
49
|
-
# Sort by count descending
|
|
50
|
-
sorted_emails = sorted(email_counts.items(), key=lambda x: x[1], reverse=True)
|
|
51
|
-
|
|
52
|
-
return [email for email, _ in sorted_emails[:count]]
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def email_to_github_username(email: str) -> str | None:
|
|
56
|
-
"""Try to map a git email to a GitHub username.
|
|
57
|
-
|
|
58
|
-
Extracts from noreply emails directly. For other emails,
|
|
59
|
-
uses the GitHub search-by-email API via gh CLI.
|
|
60
|
-
"""
|
|
61
|
-
# Handle GitHub noreply emails directly
|
|
62
|
-
if email.endswith("@users.noreply.github.com"):
|
|
63
|
-
# Format: 12345+username@users.noreply.github.com
|
|
64
|
-
# or: username@users.noreply.github.com
|
|
65
|
-
local = email.split("@")[0]
|
|
66
|
-
if "+" in local:
|
|
67
|
-
return local.split("+", 1)[1]
|
|
68
|
-
return local
|
|
69
|
-
|
|
70
|
-
# gh api handles URL encoding; pass email directly in the query
|
|
71
|
-
output = run_cmd([
|
|
72
|
-
"gh", "api", f"search/users?q={email}+in:email",
|
|
73
|
-
"--jq", ".items[0].login",
|
|
74
|
-
])
|
|
75
|
-
return output if output else None
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def get_current_github_user() -> str | None:
|
|
79
|
-
"""Get the currently authenticated GitHub username."""
|
|
80
|
-
output = run_cmd(["gh", "api", "/user", "--jq", ".login"])
|
|
81
|
-
return output if output else None
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def main():
|
|
85
|
-
parser = argparse.ArgumentParser(
|
|
86
|
-
description="Find top git contributors for PR reviewer assignment"
|
|
87
|
-
)
|
|
88
|
-
parser.add_argument("file_path", help="Path to the file to find reviewers for")
|
|
89
|
-
parser.add_argument(
|
|
90
|
-
"--count", type=int, default=2,
|
|
91
|
-
help="Number of reviewers to find (default: 2)",
|
|
92
|
-
)
|
|
93
|
-
args = parser.parse_args()
|
|
94
|
-
|
|
95
|
-
current_user = get_current_github_user()
|
|
96
|
-
|
|
97
|
-
# Request extra candidates to compensate for self-exclusion
|
|
98
|
-
fetch_count = args.count + 1 if current_user else args.count
|
|
99
|
-
emails = get_top_authors(args.file_path, fetch_count)
|
|
100
|
-
if not emails:
|
|
101
|
-
print(json.dumps({"reviewers": [], "note": "No recent authors found"}))
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
reviewers: list[str] = []
|
|
105
|
-
for email in emails:
|
|
106
|
-
username = email_to_github_username(email)
|
|
107
|
-
if username and username != current_user:
|
|
108
|
-
reviewers.append(username)
|
|
109
|
-
|
|
110
|
-
print(json.dumps({"reviewers": reviewers[:args.count]}))
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if __name__ == "__main__":
|
|
114
|
-
main()
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.9"
|
|
4
|
-
# ///
|
|
5
|
-
"""
|
|
6
|
-
Generate summary.md and report.json from a completed sweep.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python generate_report.py <sweep-dir>
|
|
10
|
-
|
|
11
|
-
Reads the data/ subdirectory for all-findings.jsonl, verified.jsonl,
|
|
12
|
-
rejected.jsonl, patches.jsonl, and security/index.jsonl, then produces:
|
|
13
|
-
- <sweep-dir>/summary.md
|
|
14
|
-
- <sweep-dir>/data/report.json
|
|
15
|
-
"""
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import argparse
|
|
19
|
-
import json
|
|
20
|
-
import os
|
|
21
|
-
import sys
|
|
22
|
-
from datetime import datetime, timezone
|
|
23
|
-
from typing import Any
|
|
24
|
-
|
|
25
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
26
|
-
from _utils import read_json, read_jsonl, severity_badge # noqa: E402
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def generate_summary_md(
|
|
30
|
-
manifest: dict[str, Any],
|
|
31
|
-
scan_index: list[dict[str, Any]],
|
|
32
|
-
all_findings: list[dict[str, Any]],
|
|
33
|
-
verified: list[dict[str, Any]],
|
|
34
|
-
rejected: list[dict[str, Any]],
|
|
35
|
-
patches: list[dict[str, Any]],
|
|
36
|
-
security_index: list[dict[str, Any]],
|
|
37
|
-
) -> str:
|
|
38
|
-
"""Generate the summary.md content."""
|
|
39
|
-
run_id = manifest.get("runId", "unknown")
|
|
40
|
-
started_at = manifest.get("startedAt", "unknown")
|
|
41
|
-
repo = manifest.get("repo", "unknown")
|
|
42
|
-
completed_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
43
|
-
|
|
44
|
-
files_scanned = sum(1 for e in scan_index if e.get("status") == "complete")
|
|
45
|
-
files_timed_out = sum(
|
|
46
|
-
1 for e in scan_index
|
|
47
|
-
if e.get("status") == "error" and e.get("error") == "timeout"
|
|
48
|
-
)
|
|
49
|
-
files_errored = sum(
|
|
50
|
-
1 for e in scan_index
|
|
51
|
-
if e.get("status") == "error" and e.get("error") != "timeout"
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
prs_created = sum(1 for p in patches if p.get("status") == "created")
|
|
55
|
-
prs_failed = sum(1 for p in patches if p.get("status") == "error")
|
|
56
|
-
|
|
57
|
-
# Severity breakdown of verified findings
|
|
58
|
-
by_severity: dict[str, int] = {}
|
|
59
|
-
for f in verified:
|
|
60
|
-
sev = f.get("severity", "info")
|
|
61
|
-
by_severity[sev] = by_severity.get(sev, 0) + 1
|
|
62
|
-
|
|
63
|
-
lines = [
|
|
64
|
-
f"# Warden Sweep: `{run_id}`",
|
|
65
|
-
"",
|
|
66
|
-
f"**Repo**: {repo}",
|
|
67
|
-
f"**Started**: {started_at}",
|
|
68
|
-
f"**Completed**: {completed_at}",
|
|
69
|
-
"",
|
|
70
|
-
"## Stats",
|
|
71
|
-
"",
|
|
72
|
-
f"| Metric | Count |",
|
|
73
|
-
f"|--------|-------|",
|
|
74
|
-
f"| Files scanned | {files_scanned} |",
|
|
75
|
-
f"| Files timed out | {files_timed_out} |",
|
|
76
|
-
f"| Files errored | {files_errored} |",
|
|
77
|
-
f"| Total findings | {len(all_findings)} |",
|
|
78
|
-
f"| Verified | {len(verified)} |",
|
|
79
|
-
f"| Rejected | {len(rejected)} |",
|
|
80
|
-
f"| PRs created | {prs_created} |",
|
|
81
|
-
f"| PRs failed | {prs_failed} |",
|
|
82
|
-
f"| Security findings | {len(security_index)} |",
|
|
83
|
-
"",
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
if by_severity:
|
|
87
|
-
lines.append("### By Severity")
|
|
88
|
-
lines.append("")
|
|
89
|
-
for sev in ["critical", "high", "medium", "low", "info"]:
|
|
90
|
-
count = by_severity.get(sev, 0)
|
|
91
|
-
if count > 0:
|
|
92
|
-
lines.append(f"- {severity_badge(sev)}: {count}")
|
|
93
|
-
lines.append("")
|
|
94
|
-
|
|
95
|
-
# Security callout
|
|
96
|
-
if security_index:
|
|
97
|
-
lines.append("## Security Findings")
|
|
98
|
-
lines.append("")
|
|
99
|
-
lines.append("The following findings are security-related and may need priority review:")
|
|
100
|
-
lines.append("")
|
|
101
|
-
lines.append("| ID | Severity | Skill | File | Title |")
|
|
102
|
-
lines.append("|----|----------|-------|------|-------|")
|
|
103
|
-
for sf in security_index:
|
|
104
|
-
fid = sf.get("findingId", "")
|
|
105
|
-
sev = severity_badge(sf.get("severity", "info"))
|
|
106
|
-
skill = sf.get("skill", "")
|
|
107
|
-
filepath = sf.get("file", "")
|
|
108
|
-
title = sf.get("title", "")
|
|
109
|
-
lines.append(f"| `{fid}` | {sev} | {skill} | `{filepath}` | {title} |")
|
|
110
|
-
lines.append("")
|
|
111
|
-
|
|
112
|
-
# Verified findings table
|
|
113
|
-
if verified:
|
|
114
|
-
lines.append("## Verified Findings")
|
|
115
|
-
lines.append("")
|
|
116
|
-
lines.append("| ID | Severity | Skill | File | Title | PR |")
|
|
117
|
-
lines.append("|----|----------|-------|------|-------|-----|")
|
|
118
|
-
|
|
119
|
-
# Build patches lookup
|
|
120
|
-
pr_lookup: dict[str, str] = {}
|
|
121
|
-
for p in patches:
|
|
122
|
-
if p.get("status") == "created" and p.get("findingId"):
|
|
123
|
-
pr_lookup[p["findingId"]] = p.get("prUrl", "")
|
|
124
|
-
|
|
125
|
-
for f in verified:
|
|
126
|
-
fid = f.get("findingId", "")
|
|
127
|
-
sev = severity_badge(f.get("severity", "info"))
|
|
128
|
-
skill = f.get("skill", "")
|
|
129
|
-
filepath = f.get("file", "")
|
|
130
|
-
title = f.get("title", "")
|
|
131
|
-
pr_url = pr_lookup.get(fid, "")
|
|
132
|
-
pr_link = f"[PR]({pr_url})" if pr_url else "-"
|
|
133
|
-
lines.append(f"| `{fid}` | {sev} | {skill} | `{filepath}` | {title} | {pr_link} |")
|
|
134
|
-
lines.append("")
|
|
135
|
-
|
|
136
|
-
# Rejected findings summary
|
|
137
|
-
if rejected:
|
|
138
|
-
lines.append(f"## Rejected Findings ({len(rejected)})")
|
|
139
|
-
lines.append("")
|
|
140
|
-
lines.append("These findings were evaluated and determined to be false positives.")
|
|
141
|
-
lines.append("See `data/rejected.jsonl` for details.")
|
|
142
|
-
lines.append("")
|
|
143
|
-
|
|
144
|
-
lines.append("---")
|
|
145
|
-
lines.append(f"*Generated by Warden Sweep `{run_id}`*")
|
|
146
|
-
|
|
147
|
-
return "\n".join(lines) + "\n"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def generate_report_json(
|
|
151
|
-
manifest: dict[str, Any],
|
|
152
|
-
scan_index: list[dict[str, Any]],
|
|
153
|
-
all_findings: list[dict[str, Any]],
|
|
154
|
-
verified: list[dict[str, Any]],
|
|
155
|
-
rejected: list[dict[str, Any]],
|
|
156
|
-
patches: list[dict[str, Any]],
|
|
157
|
-
security_index: list[dict[str, Any]],
|
|
158
|
-
) -> dict[str, Any]:
|
|
159
|
-
"""Generate the report.json data."""
|
|
160
|
-
run_id = manifest.get("runId", "unknown")
|
|
161
|
-
completed_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
162
|
-
|
|
163
|
-
files_scanned = sum(1 for e in scan_index if e.get("status") == "complete")
|
|
164
|
-
files_timed_out = sum(
|
|
165
|
-
1 for e in scan_index
|
|
166
|
-
if e.get("status") == "error" and e.get("error") == "timeout"
|
|
167
|
-
)
|
|
168
|
-
files_errored = sum(
|
|
169
|
-
1 for e in scan_index
|
|
170
|
-
if e.get("status") == "error" and e.get("error") != "timeout"
|
|
171
|
-
)
|
|
172
|
-
prs_created = sum(1 for p in patches if p.get("status") == "created")
|
|
173
|
-
prs_failed = sum(1 for p in patches if p.get("status") == "error")
|
|
174
|
-
|
|
175
|
-
# Count verify errors (findings in all but not in verified or rejected)
|
|
176
|
-
verified_ids = {f["findingId"] for f in verified if "findingId" in f}
|
|
177
|
-
rejected_ids = {f["findingId"] for f in rejected if "findingId" in f}
|
|
178
|
-
all_ids = {f["findingId"] for f in all_findings if "findingId" in f}
|
|
179
|
-
verify_errors = len(all_ids - verified_ids - rejected_ids)
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
"runId": run_id,
|
|
183
|
-
"completedAt": completed_at,
|
|
184
|
-
"scan": {
|
|
185
|
-
"filesScanned": files_scanned,
|
|
186
|
-
"filesTimedOut": files_timed_out,
|
|
187
|
-
"filesErrored": files_errored,
|
|
188
|
-
"totalFindings": len(all_findings),
|
|
189
|
-
},
|
|
190
|
-
"verify": {
|
|
191
|
-
"verified": len(verified),
|
|
192
|
-
"rejected": len(rejected),
|
|
193
|
-
"errors": verify_errors,
|
|
194
|
-
},
|
|
195
|
-
"patch": {
|
|
196
|
-
"prsCreated": prs_created,
|
|
197
|
-
"prsFailed": prs_failed,
|
|
198
|
-
},
|
|
199
|
-
"security": {
|
|
200
|
-
"count": len(security_index),
|
|
201
|
-
},
|
|
202
|
-
"prs": [
|
|
203
|
-
{
|
|
204
|
-
"findingId": p.get("findingId", ""),
|
|
205
|
-
"url": p.get("prUrl", ""),
|
|
206
|
-
"severity": next(
|
|
207
|
-
(f.get("severity", "") for f in verified if f.get("findingId") == p.get("findingId")),
|
|
208
|
-
"",
|
|
209
|
-
),
|
|
210
|
-
}
|
|
211
|
-
for p in patches
|
|
212
|
-
if p.get("status") == "created"
|
|
213
|
-
],
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def main():
|
|
218
|
-
parser = argparse.ArgumentParser(
|
|
219
|
-
description="Generate sweep summary and report"
|
|
220
|
-
)
|
|
221
|
-
parser.add_argument("sweep_dir", help="Path to the sweep output directory")
|
|
222
|
-
args = parser.parse_args()
|
|
223
|
-
|
|
224
|
-
sweep_dir = args.sweep_dir
|
|
225
|
-
data_dir = os.path.join(sweep_dir, "data")
|
|
226
|
-
|
|
227
|
-
# Read inputs
|
|
228
|
-
manifest = read_json(os.path.join(data_dir, "manifest.json")) or {}
|
|
229
|
-
scan_index = read_jsonl(os.path.join(data_dir, "scan-index.jsonl"))
|
|
230
|
-
all_findings = read_jsonl(os.path.join(data_dir, "all-findings.jsonl"))
|
|
231
|
-
verified = read_jsonl(os.path.join(data_dir, "verified.jsonl"))
|
|
232
|
-
rejected = read_jsonl(os.path.join(data_dir, "rejected.jsonl"))
|
|
233
|
-
patches = read_jsonl(os.path.join(data_dir, "patches.jsonl"))
|
|
234
|
-
security_index = read_jsonl(os.path.join(sweep_dir, "security", "index.jsonl"))
|
|
235
|
-
|
|
236
|
-
# Generate summary.md
|
|
237
|
-
summary_md = generate_summary_md(
|
|
238
|
-
manifest, scan_index,
|
|
239
|
-
all_findings, verified, rejected, patches, security_index,
|
|
240
|
-
)
|
|
241
|
-
summary_path = os.path.join(sweep_dir, "summary.md")
|
|
242
|
-
with open(summary_path, "w") as f:
|
|
243
|
-
f.write(summary_md)
|
|
244
|
-
|
|
245
|
-
# Generate report.json
|
|
246
|
-
report = generate_report_json(
|
|
247
|
-
manifest, scan_index, all_findings,
|
|
248
|
-
verified, rejected, patches, security_index,
|
|
249
|
-
)
|
|
250
|
-
report_path = os.path.join(data_dir, "report.json")
|
|
251
|
-
with open(report_path, "w") as f:
|
|
252
|
-
json.dump(report, f, indent=2)
|
|
253
|
-
f.write("\n")
|
|
254
|
-
|
|
255
|
-
print(json.dumps({
|
|
256
|
-
"summaryPath": summary_path,
|
|
257
|
-
"reportPath": report_path,
|
|
258
|
-
"verified": len(verified),
|
|
259
|
-
"rejected": len(rejected),
|
|
260
|
-
"prsCreated": report["patch"]["prsCreated"],
|
|
261
|
-
"securityFindings": len(security_index),
|
|
262
|
-
}))
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if __name__ == "__main__":
|
|
266
|
-
main()
|