reposec 0.1.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/LICENSE +21 -0
- package/README.md +733 -0
- package/SECURITY.md +29 -0
- package/bin/reposec.mjs +20 -0
- package/lib/baseline.ts +100 -0
- package/lib/client-bundle.ts +202 -0
- package/lib/exporters.ts +509 -0
- package/lib/fingerprint.ts +5 -0
- package/lib/github.ts +365 -0
- package/lib/local-repo.ts +182 -0
- package/lib/rules.ts +662 -0
- package/lib/scan-targets.ts +155 -0
- package/lib/scanner.ts +2015 -0
- package/lib/scoring.ts +58 -0
- package/lib/types.ts +133 -0
- package/lib/utils.ts +24 -0
- package/lib/verification.ts +67 -0
- package/package.json +66 -0
- package/scripts/reposec-cli.mts +195 -0
package/lib/exporters.ts
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CategoryBreakdown,
|
|
3
|
+
CheckResult,
|
|
4
|
+
FileGroup,
|
|
5
|
+
Finding,
|
|
6
|
+
FindingCategory,
|
|
7
|
+
ScanReport,
|
|
8
|
+
Severity,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import { BAND_LABELS, groupBySeverity, SEVERITY_ORDER } from "./scoring";
|
|
11
|
+
|
|
12
|
+
const HEADING = (text: string) => `${text}\n${"-".repeat(text.length)}`;
|
|
13
|
+
|
|
14
|
+
function severityLabel(s: Severity): string {
|
|
15
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function checkStatus(s: CheckResult["status"]): string {
|
|
19
|
+
switch (s) {
|
|
20
|
+
case "pass":
|
|
21
|
+
return "PASS";
|
|
22
|
+
case "fail":
|
|
23
|
+
return "FAIL";
|
|
24
|
+
case "warn":
|
|
25
|
+
return "WARN";
|
|
26
|
+
case "missing":
|
|
27
|
+
return "MISSING";
|
|
28
|
+
case "info":
|
|
29
|
+
return "INFO";
|
|
30
|
+
case "skip":
|
|
31
|
+
return "SKIP";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findingLine(f: Finding): string {
|
|
36
|
+
const loc = f.file
|
|
37
|
+
? f.line
|
|
38
|
+
? `\`${f.file}:${f.line}\``
|
|
39
|
+
: `\`${f.file}\``
|
|
40
|
+
: "_general_";
|
|
41
|
+
const confidence = f.confidence
|
|
42
|
+
? `, ${f.verified ? "verified" : `${f.confidence} confidence`}`
|
|
43
|
+
: "";
|
|
44
|
+
return `- **${f.title}** (${severityLabel(f.severity)}${confidence}) \u2014 ${loc}\n - _Why:_ ${f.description}\n - _Fix:_ ${f.fix}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function categoryLabel(c: FindingCategory): string {
|
|
48
|
+
switch (c) {
|
|
49
|
+
case "environment":
|
|
50
|
+
return "Environment & secrets";
|
|
51
|
+
case "documentation":
|
|
52
|
+
return "Documentation";
|
|
53
|
+
case "package":
|
|
54
|
+
return "Package & scripts";
|
|
55
|
+
case "github":
|
|
56
|
+
return "GitHub features";
|
|
57
|
+
case "secret":
|
|
58
|
+
return "Secret patterns";
|
|
59
|
+
case "docker":
|
|
60
|
+
return "Container hygiene";
|
|
61
|
+
case "community":
|
|
62
|
+
return "Community health";
|
|
63
|
+
case "ci":
|
|
64
|
+
return "CI quality";
|
|
65
|
+
case "metadata":
|
|
66
|
+
return "Repository metadata";
|
|
67
|
+
case "code":
|
|
68
|
+
return "Source code patterns";
|
|
69
|
+
case "dependencies":
|
|
70
|
+
return "Dependency hygiene";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatDate(iso: string): string {
|
|
75
|
+
try {
|
|
76
|
+
return new Date(iso).toUTCString();
|
|
77
|
+
} catch {
|
|
78
|
+
return iso;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatDuration(ms: number): string {
|
|
83
|
+
if (ms < 1000) return `${ms} ms`;
|
|
84
|
+
return `${(ms / 1000).toFixed(2)} s`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function repoMetadataBlock(report: ScanReport): string {
|
|
88
|
+
const r = report.repo;
|
|
89
|
+
const lines: string[] = [];
|
|
90
|
+
lines.push(`- **Repository:** [${r.owner}/${r.repo}](${r.htmlUrl})`);
|
|
91
|
+
lines.push(`- **Description:** ${r.description?.trim() || "_(none)_"}`);
|
|
92
|
+
if (r.language) lines.push(`- **Primary language:** ${r.language}`);
|
|
93
|
+
if (r.licenseSpdxId)
|
|
94
|
+
lines.push(`- **License:** ${r.licenseSpdxId}${r.licenseName ? ` (${r.licenseName})` : ""}`);
|
|
95
|
+
lines.push(`- **Default branch:** \`${r.defaultBranch}\``);
|
|
96
|
+
lines.push(
|
|
97
|
+
`- **Stars / forks / open issues:** ${(r.stars ?? 0).toLocaleString()} / ${(r.forks ?? 0).toLocaleString()} / ${(r.openIssues ?? 0).toLocaleString()}`,
|
|
98
|
+
);
|
|
99
|
+
if (typeof r.sizeKb === "number")
|
|
100
|
+
lines.push(`- **Repository size:** ${(r.sizeKb / 1024).toFixed(2)} MB`);
|
|
101
|
+
if (r.pushedAt) lines.push(`- **Last push:** ${formatDate(r.pushedAt)}`);
|
|
102
|
+
if (r.archived) lines.push(`- **Archived:** yes`);
|
|
103
|
+
if (r.isTemplate) lines.push(`- **Template repository:** yes`);
|
|
104
|
+
if (r.topics && r.topics.length > 0)
|
|
105
|
+
lines.push(`- **Topics:** ${r.topics.map((t) => `\`${t}\``).join(", ")}`);
|
|
106
|
+
lines.push(`- **Scanned at:** ${formatDate(report.scannedAt)}`);
|
|
107
|
+
lines.push(`- **Duration:** ${formatDuration(report.durationMs)}`);
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function categoryBreakdownBlock(by: Record<FindingCategory, CategoryBreakdown>): string {
|
|
112
|
+
return Object.entries(by)
|
|
113
|
+
.filter(([, v]) => v.total > 0)
|
|
114
|
+
.map(([k, v]) => {
|
|
115
|
+
const ratio = v.total === 0 ? 0 : Math.round((v.passed / v.total) * 100);
|
|
116
|
+
return `- **${categoryLabel(k as FindingCategory)}:** ${v.passed} / ${v.total} passed (${ratio}%)`;
|
|
117
|
+
})
|
|
118
|
+
.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checksTableBlock(checks: CheckResult[]): string {
|
|
122
|
+
const rows = checks
|
|
123
|
+
.map(
|
|
124
|
+
(c) =>
|
|
125
|
+
`| ${checkStatus(c.status)} | ${categoryLabel(c.category)} | ${c.title} | ${c.file ? `\`${c.file}\`` : "_\u2014_"} | ${c.message.replace(/\|/g, "\\|").slice(0, 200)} |`,
|
|
126
|
+
)
|
|
127
|
+
.join("\n");
|
|
128
|
+
return [
|
|
129
|
+
`| Status | Category | Check | File | Detail |`,
|
|
130
|
+
`| ------ | -------- | ----- | ---- | ------ |`,
|
|
131
|
+
rows,
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function findingsByFileBlock(groups: FileGroup[]): string {
|
|
136
|
+
if (groups.length === 0) return "_No file-level findings._";
|
|
137
|
+
return groups
|
|
138
|
+
.map((g) => {
|
|
139
|
+
const counts = SEVERITY_ORDER.filter((s) => g.counts[s] > 0)
|
|
140
|
+
.map((s) => `${g.counts[s]} ${severityLabel(s).toLowerCase()}`)
|
|
141
|
+
.join(", ");
|
|
142
|
+
return [
|
|
143
|
+
`### \`${g.path}\` (${g.findings.length} finding${g.findings.length === 1 ? "" : "s"} \u2014 ${counts})`,
|
|
144
|
+
``,
|
|
145
|
+
...g.findings.map(
|
|
146
|
+
(f) =>
|
|
147
|
+
`- **${f.title}** (${severityLabel(f.severity)})${f.line ? ` \u2014 line ${f.line}` : ""}\n - _Fix:_ ${f.fix}`,
|
|
148
|
+
),
|
|
149
|
+
``,
|
|
150
|
+
].join("\n");
|
|
151
|
+
})
|
|
152
|
+
.join("\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function findingsByCategoryBlock(findings: Finding[]): string {
|
|
156
|
+
const grouped = new Map<FindingCategory, Finding[]>();
|
|
157
|
+
for (const f of findings) {
|
|
158
|
+
if (!grouped.has(f.category)) grouped.set(f.category, []);
|
|
159
|
+
grouped.get(f.category)!.push(f);
|
|
160
|
+
}
|
|
161
|
+
if (grouped.size === 0) return "_No findings._";
|
|
162
|
+
return Array.from(grouped.entries())
|
|
163
|
+
.map(([cat, items]) => {
|
|
164
|
+
return [
|
|
165
|
+
`### ${categoryLabel(cat)} (${items.length})`,
|
|
166
|
+
``,
|
|
167
|
+
...items.map(findingLine),
|
|
168
|
+
``,
|
|
169
|
+
].join("\n");
|
|
170
|
+
})
|
|
171
|
+
.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function generateMarkdownReport(report: ScanReport): string {
|
|
175
|
+
const grouped = groupBySeverity(report.findings);
|
|
176
|
+
const counts = SEVERITY_ORDER.map(
|
|
177
|
+
(s) => `- ${severityLabel(s)}: ${grouped[s].length}`,
|
|
178
|
+
).join("\n");
|
|
179
|
+
|
|
180
|
+
const findingsBlock = SEVERITY_ORDER.flatMap((s) => grouped[s])
|
|
181
|
+
.map(findingLine)
|
|
182
|
+
.join("\n");
|
|
183
|
+
|
|
184
|
+
const missing = report.summary.filesMissing.length
|
|
185
|
+
? report.summary.filesMissing.map((f) => `- \`${f}\``).join("\n")
|
|
186
|
+
: "_None \u2014 all expected files are present._";
|
|
187
|
+
|
|
188
|
+
return [
|
|
189
|
+
`# RepoSec Security Report`,
|
|
190
|
+
``,
|
|
191
|
+
repoMetadataBlock(report),
|
|
192
|
+
``,
|
|
193
|
+
HEADING("Security Score"),
|
|
194
|
+
``,
|
|
195
|
+
`**Score:** ${report.score} / 100 \u2014 ${BAND_LABELS[report.scoreBand]}`,
|
|
196
|
+
``,
|
|
197
|
+
HEADING("Scan summary"),
|
|
198
|
+
``,
|
|
199
|
+
`- **Checks performed:** ${report.summary.totalChecks}`,
|
|
200
|
+
`- **Checks passed:** ${report.summary.passed}`,
|
|
201
|
+
`- **Checks failed:** ${report.summary.failed}`,
|
|
202
|
+
`- **Total findings:** ${report.summary.totalFindings}`,
|
|
203
|
+
`- **Files checked:** ${report.summary.filesChecked}`,
|
|
204
|
+
`- **Missing files:** ${report.summary.filesMissing.length}`,
|
|
205
|
+
``,
|
|
206
|
+
HEADING("Findings by severity"),
|
|
207
|
+
``,
|
|
208
|
+
counts,
|
|
209
|
+
``,
|
|
210
|
+
HEADING("Checks by category"),
|
|
211
|
+
``,
|
|
212
|
+
categoryBreakdownBlock(report.summary.byCategory),
|
|
213
|
+
``,
|
|
214
|
+
HEADING("Checks performed"),
|
|
215
|
+
``,
|
|
216
|
+
checksTableBlock(report.summary.checks),
|
|
217
|
+
``,
|
|
218
|
+
HEADING("Missing files"),
|
|
219
|
+
``,
|
|
220
|
+
missing,
|
|
221
|
+
``,
|
|
222
|
+
HEADING("Findings by file"),
|
|
223
|
+
``,
|
|
224
|
+
findingsByFileBlock(report.fileGroups),
|
|
225
|
+
``,
|
|
226
|
+
HEADING("All findings (grouped by category)"),
|
|
227
|
+
``,
|
|
228
|
+
findingsByCategoryBlock(report.findings) ||
|
|
229
|
+
"_No findings \u2014 the repository looks clean on the checks we ran._",
|
|
230
|
+
``,
|
|
231
|
+
HEADING("All findings (flat list)"),
|
|
232
|
+
``,
|
|
233
|
+
findingsBlock || "_No findings._",
|
|
234
|
+
``,
|
|
235
|
+
HEADING("Files inspected"),
|
|
236
|
+
``,
|
|
237
|
+
report.filesChecked.map((f) => `- \`${f}\``).join("\n"),
|
|
238
|
+
``,
|
|
239
|
+
HEADING("Disclaimer"),
|
|
240
|
+
``,
|
|
241
|
+
`_RepoSec is a static, rule-based scanner. Findings are hints, not guarantees.${" "}Heuristics can produce false positives; review every finding before changing production code._`,
|
|
242
|
+
].join("\n");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function generateJsonReport(report: ScanReport): string {
|
|
246
|
+
return JSON.stringify(report, null, 2);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function sarifLevel(severity: Severity): "error" | "warning" | "note" {
|
|
250
|
+
if (severity === "critical" || severity === "high") return "error";
|
|
251
|
+
if (severity === "medium" || severity === "low") return "warning";
|
|
252
|
+
return "note";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function sarifRuleId(finding: Finding): string {
|
|
256
|
+
const base = finding.id.split("-").slice(0, 3).join("-");
|
|
257
|
+
return base || finding.category;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function generateSarifReport(report: ScanReport): string {
|
|
261
|
+
const rules = new Map<string, Finding>();
|
|
262
|
+
for (const finding of report.findings) {
|
|
263
|
+
const id = sarifRuleId(finding);
|
|
264
|
+
if (!rules.has(id)) rules.set(id, finding);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return JSON.stringify(
|
|
268
|
+
{
|
|
269
|
+
version: "2.1.0",
|
|
270
|
+
$schema:
|
|
271
|
+
"https://json.schemastore.org/sarif-2.1.0.json",
|
|
272
|
+
runs: [
|
|
273
|
+
{
|
|
274
|
+
tool: {
|
|
275
|
+
driver: {
|
|
276
|
+
name: "RepoSec",
|
|
277
|
+
informationUri: "https://github.com/zanesense/reposec",
|
|
278
|
+
rules: Array.from(rules.entries()).map(([id, finding]) => ({
|
|
279
|
+
id,
|
|
280
|
+
name: finding.title,
|
|
281
|
+
shortDescription: { text: finding.title },
|
|
282
|
+
fullDescription: { text: finding.description },
|
|
283
|
+
help: { text: finding.fix },
|
|
284
|
+
properties: {
|
|
285
|
+
category: finding.category,
|
|
286
|
+
severity: finding.severity,
|
|
287
|
+
confidence: finding.confidence ?? "medium",
|
|
288
|
+
},
|
|
289
|
+
})),
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
results: report.findings.map((finding) => ({
|
|
293
|
+
ruleId: sarifRuleId(finding),
|
|
294
|
+
level: sarifLevel(finding.severity),
|
|
295
|
+
message: {
|
|
296
|
+
text: `${finding.title}: ${finding.description}`,
|
|
297
|
+
},
|
|
298
|
+
locations: finding.file
|
|
299
|
+
? [
|
|
300
|
+
{
|
|
301
|
+
physicalLocation: {
|
|
302
|
+
artifactLocation: { uri: finding.file },
|
|
303
|
+
region: finding.line
|
|
304
|
+
? { startLine: finding.line }
|
|
305
|
+
: undefined,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
]
|
|
309
|
+
: [],
|
|
310
|
+
fingerprints: finding.fingerprint
|
|
311
|
+
? { secretFingerprint: finding.fingerprint }
|
|
312
|
+
: undefined,
|
|
313
|
+
properties: {
|
|
314
|
+
category: finding.category,
|
|
315
|
+
severity: finding.severity,
|
|
316
|
+
confidence: finding.confidence ?? "medium",
|
|
317
|
+
verified: finding.verified ?? false,
|
|
318
|
+
evidence: finding.evidence,
|
|
319
|
+
remediation: finding.fix,
|
|
320
|
+
},
|
|
321
|
+
})),
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
null,
|
|
326
|
+
2,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function generateSecurityMdTemplate(): string {
|
|
331
|
+
return `# Security Policy
|
|
332
|
+
|
|
333
|
+
## Supported Versions
|
|
334
|
+
|
|
335
|
+
| Version | Supported |
|
|
336
|
+
| ------- | ------------------ |
|
|
337
|
+
| latest | :white_check_mark: |
|
|
338
|
+
| older | :x: |
|
|
339
|
+
|
|
340
|
+
## Reporting a Vulnerability
|
|
341
|
+
|
|
342
|
+
If you discover a security issue, please email **security@example.com** instead of
|
|
343
|
+
opening a public issue. Include:
|
|
344
|
+
|
|
345
|
+
- A clear description of the issue
|
|
346
|
+
- Steps to reproduce
|
|
347
|
+
- The impact you believe it has
|
|
348
|
+
- Any suggested fix (optional)
|
|
349
|
+
|
|
350
|
+
We aim to acknowledge new reports within **3 business days** and ship a fix or
|
|
351
|
+
mitigation within **30 days** for high-severity issues.
|
|
352
|
+
|
|
353
|
+
## Scope
|
|
354
|
+
|
|
355
|
+
This policy applies to the code in this repository and any official packages we
|
|
356
|
+
publish. It does not cover social engineering, physical attacks, or denial of
|
|
357
|
+
service.
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function generateEnvExampleTemplate(): string {
|
|
362
|
+
return `# Copy this file to .env and fill in the values for your environment.
|
|
363
|
+
# Never commit a real .env file. Keep secrets out of version control.
|
|
364
|
+
|
|
365
|
+
# --- Core ---
|
|
366
|
+
NODE_ENV=development
|
|
367
|
+
PORT=3000
|
|
368
|
+
|
|
369
|
+
# --- Database ---
|
|
370
|
+
# DATABASE_URL=postgres://user:password@localhost:5432/app
|
|
371
|
+
|
|
372
|
+
# --- Auth ---
|
|
373
|
+
# JWT_SECRET=replace-with-a-long-random-string
|
|
374
|
+
# SESSION_SECRET=replace-with-a-long-random-string
|
|
375
|
+
|
|
376
|
+
# --- External APIs ---
|
|
377
|
+
# OPENAI_API_KEY=
|
|
378
|
+
# STRIPE_SECRET_KEY=
|
|
379
|
+
# GITHUB_TOKEN=
|
|
380
|
+
`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function generateIssueChecklist(report: ScanReport): string {
|
|
384
|
+
const grouped = groupBySeverity(report.findings);
|
|
385
|
+
const items = SEVERITY_ORDER.flatMap((s) => grouped[s]);
|
|
386
|
+
const checklist = items
|
|
387
|
+
.map(
|
|
388
|
+
(f) =>
|
|
389
|
+
`- [ ] **${severityLabel(f.severity)}** \u2014 ${f.title}${
|
|
390
|
+
f.file ? ` _(${f.file}${f.line ? `:${f.line}` : ""})_` : ""
|
|
391
|
+
}`,
|
|
392
|
+
)
|
|
393
|
+
.join("\n");
|
|
394
|
+
|
|
395
|
+
return [
|
|
396
|
+
`## RepoSec follow-up`,
|
|
397
|
+
``,
|
|
398
|
+
`Repository: ${report.repo.owner}/${report.repo.repo}`,
|
|
399
|
+
`Score: **${report.score} / 100** (${BAND_LABELS[report.scoreBand]})`,
|
|
400
|
+
``,
|
|
401
|
+
`### Checklist`,
|
|
402
|
+
``,
|
|
403
|
+
checklist || `- [ ] No findings \u2014 the repository looks clean on the checks we ran.`,
|
|
404
|
+
``,
|
|
405
|
+
`### Suggested order`,
|
|
406
|
+
``,
|
|
407
|
+
`1. Rotate or remove any exposed secrets.`,
|
|
408
|
+
`2. Add the missing hygiene files (SECURITY.md, .env.example, .gitignore).`,
|
|
409
|
+
`3. Wire up Dependabot and a basic CI workflow.`,
|
|
410
|
+
`4. Document setup and environment variables in the README.`,
|
|
411
|
+
].join("\n");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function generateFixPrompt(report: ScanReport): string {
|
|
415
|
+
const grouped = groupBySeverity(report.findings);
|
|
416
|
+
const top = SEVERITY_ORDER.flatMap((s) => grouped[s]).slice(0, 12);
|
|
417
|
+
|
|
418
|
+
const items = top
|
|
419
|
+
.map(
|
|
420
|
+
(f, i) =>
|
|
421
|
+
`${i + 1}. **${f.title}** (${severityLabel(f.severity)})${
|
|
422
|
+
f.file ? ` \u2014 \`${f.file}${f.line ? `:${f.line}` : ""}\`` : ""
|
|
423
|
+
}\n Fix: ${f.fix}`,
|
|
424
|
+
)
|
|
425
|
+
.join("\n");
|
|
426
|
+
|
|
427
|
+
return `You are a defensive security assistant. Review this RepoSec report and fix the high-risk items first.
|
|
428
|
+
|
|
429
|
+
Repository: ${report.repo.owner}/${report.repo.repo}
|
|
430
|
+
Score: ${report.score} / 100 (${BAND_LABELS[report.scoreBand]})
|
|
431
|
+
|
|
432
|
+
Top issues:
|
|
433
|
+
${items || "- No findings \u2014 the repository looks clean."}
|
|
434
|
+
|
|
435
|
+
Rules:
|
|
436
|
+
- Do not rewrite unrelated files.
|
|
437
|
+
- Do not expose secrets, tokens, or credentials in source.
|
|
438
|
+
- Use placeholders for any environment variable value (e.g. KEY=value).
|
|
439
|
+
- Add .env.example with placeholder values only.
|
|
440
|
+
- Update .gitignore to cover .env, build output, and dependencies.
|
|
441
|
+
- Add SECURITY.md with responsible-disclosure instructions.
|
|
442
|
+
- Improve README setup and environment variable sections.
|
|
443
|
+
- Explain every change after editing.
|
|
444
|
+
|
|
445
|
+
When you are done, summarize what you changed and what still needs a human review.`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface DownloadPayload {
|
|
449
|
+
filename: string;
|
|
450
|
+
mime: string;
|
|
451
|
+
content: string;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function buildDownload(
|
|
455
|
+
report: ScanReport,
|
|
456
|
+
kind:
|
|
457
|
+
| "report"
|
|
458
|
+
| "security-md"
|
|
459
|
+
| "env-example"
|
|
460
|
+
| "issue"
|
|
461
|
+
| "fix-prompt"
|
|
462
|
+
| "json"
|
|
463
|
+
| "sarif",
|
|
464
|
+
): DownloadPayload {
|
|
465
|
+
switch (kind) {
|
|
466
|
+
case "report":
|
|
467
|
+
return {
|
|
468
|
+
filename: `reposec-${report.repo.owner}-${report.repo.repo}.md`,
|
|
469
|
+
mime: "text/markdown",
|
|
470
|
+
content: generateMarkdownReport(report),
|
|
471
|
+
};
|
|
472
|
+
case "security-md":
|
|
473
|
+
return {
|
|
474
|
+
filename: "SECURITY.md",
|
|
475
|
+
mime: "text/markdown",
|
|
476
|
+
content: generateSecurityMdTemplate(),
|
|
477
|
+
};
|
|
478
|
+
case "env-example":
|
|
479
|
+
return {
|
|
480
|
+
filename: ".env.example",
|
|
481
|
+
mime: "text/plain",
|
|
482
|
+
content: generateEnvExampleTemplate(),
|
|
483
|
+
};
|
|
484
|
+
case "issue":
|
|
485
|
+
return {
|
|
486
|
+
filename: `reposec-issues-${report.repo.owner}-${report.repo.repo}.md`,
|
|
487
|
+
mime: "text/markdown",
|
|
488
|
+
content: generateIssueChecklist(report),
|
|
489
|
+
};
|
|
490
|
+
case "fix-prompt":
|
|
491
|
+
return {
|
|
492
|
+
filename: `reposec-fix-prompt.md`,
|
|
493
|
+
mime: "text/markdown",
|
|
494
|
+
content: generateFixPrompt(report),
|
|
495
|
+
};
|
|
496
|
+
case "json":
|
|
497
|
+
return {
|
|
498
|
+
filename: `reposec-${report.repo.owner}-${report.repo.repo}.json`,
|
|
499
|
+
mime: "application/json",
|
|
500
|
+
content: generateJsonReport(report),
|
|
501
|
+
};
|
|
502
|
+
case "sarif":
|
|
503
|
+
return {
|
|
504
|
+
filename: `reposec-${report.repo.owner}-${report.repo.repo}.sarif`,
|
|
505
|
+
mime: "application/sarif+json",
|
|
506
|
+
content: generateSarifReport(report),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|