patchproof-tui 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # PatchProof TUI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ After the package is published to npm, install it globally:
4
+
5
+ ```sh
6
+ npm install --global patchproof-tui
7
+ patchproof-tui
8
+ ```
9
+
10
+ Requires Node.js 18+ and a running PatchProof API (`cargo run -p patchproof-api` from the project checkout). Configure the endpoint and API key as needed:
11
+
12
+ ```sh
13
+ PATCHPROOF_API=http://127.0.0.1:8000 PATCHPROOF_API_KEY=your-key patchproof-tui
14
+ ```
15
+
16
+ In PowerShell:
17
+
18
+ ```powershell
19
+ $env:PATCHPROOF_API = 'http://127.0.0.1:8000'
20
+ $env:PATCHPROOF_API_KEY = 'your-key'
21
+ patchproof-tui
22
+ ```
23
+
24
+ The terminal app starts scans, polls progress, prints findings and verification results, saves Markdown security reports, and exports generated fixes as patch bundles. Patch bundles are proposals; review them before applying. The PatchProof pipeline also applies and verifies fixes in its scan checkout.
25
+
26
+ To publish a release, authenticate with the npm account that owns the package name and run `npm publish` from this directory. Consumers can install it once published.
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from 'node:readline/promises'
3
+ import { stdin, stdout } from 'node:process'
4
+ import { writeFile } from 'node:fs/promises'
5
+ import { resolve } from 'node:path'
6
+
7
+ const api = (process.env.PATCHPROOF_API || 'http://127.0.0.1:8000').replace(/\/$/, '')
8
+ const key = process.env.PATCHPROOF_API_KEY || ''
9
+ const terminal = createInterface({ input: stdin, output: stdout })
10
+ const sleep = (ms) => new Promise((done) => setTimeout(done, ms))
11
+
12
+ function redact(value) {
13
+ return String(value ?? '')
14
+ .replace(/(https?:\/\/)[^/@\s]+@/gi, '$1[REDACTED]@')
15
+ .replace(/\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})\b/g, '[REDACTED]')
16
+ .replace(/(api[_-]?key|secret|password|token|authorization)(\s*[:=]\s*)(["']?)[^\s"'`,;]+/gi, '$1$2[REDACTED]')
17
+ }
18
+
19
+ async function request(path, { method = 'GET', body } = {}) {
20
+ const response = await fetch(`${api}${path}`, {
21
+ method,
22
+ headers: { 'Content-Type': 'application/json', ...(key ? { 'X-API-Key': key } : {}) },
23
+ ...(body ? { body: JSON.stringify(body) } : {}),
24
+ signal: AbortSignal.timeout(35_000),
25
+ })
26
+ const text = await response.text()
27
+ if (!response.ok) throw new Error(`${response.status}: ${text}`)
28
+ return text ? JSON.parse(text) : {}
29
+ }
30
+
31
+ function printScan(scan) {
32
+ const findings = scan.findings || []
33
+ console.log(`\n${scan.id} · ${scan.status} · ${redact(scan.repo_url)} · ${findings.length} finding(s)`)
34
+ findings.forEach((finding, index) => {
35
+ const patch = finding.patches?.[0]
36
+ const verified = patch?.verifications?.at(-1)?.status || 'not verified'
37
+ console.log(`${String(index + 1).padStart(2)}. ${(finding.severity || '?').toUpperCase().padEnd(8)} risk ${(finding.risk_score ?? 0).toFixed(1)} · ${redact(finding.title)} (${finding.cwe || 'n/a'})`)
38
+ console.log(` ${redact(finding.file_path)}:${finding.line_no} · reproduced: ${Boolean(finding.reproduced)} · fix: ${verified}`)
39
+ })
40
+ }
41
+
42
+ function makeReport(scan) {
43
+ const findings = scan.findings || []
44
+ const fixed = findings.filter((f) => f.patches?.some((p) => p.verifications?.some((v) => v.status === 'fixed'))).length
45
+ const lines = [
46
+ '# PatchProof security report', '',
47
+ `- Repository: ${redact(scan.repo_url)}`, `- Branch: ${redact(scan.branch || 'HEAD')}`,
48
+ `- Scan: ${scan.id}`, `- Status: ${scan.status}`, `- Findings: ${findings.length} (${fixed} verified fixed)`, '',
49
+ '## Findings', '',
50
+ ]
51
+ if (!findings.length) lines.push('No findings were recorded.', '')
52
+ for (const f of findings) {
53
+ const patch = f.patches?.[0]
54
+ const verification = patch?.verifications?.at(-1)
55
+ lines.push(`### ${redact((f.severity || 'unknown').toUpperCase())}: ${redact(f.title)}`, '',
56
+ `- Rule / CWE: ${redact(f.rule_id || 'n/a')} / ${redact(f.cwe || 'n/a')}`,
57
+ `- Location: \`${redact(f.file_path)}:${f.line_no}\``,
58
+ `- Risk score: ${f.risk_score ?? 'n/a'} / 10`,
59
+ `- Reproduced: ${f.reproduced ? 'yes' : 'no'}`,
60
+ `- Fix verification: ${verification?.status || 'not verified'}`, '',
61
+ redact(f.explanation || 'No explanation provided.'), '',
62
+ 'Review the source location and scan evidence for reproduction details.', '')
63
+ if (patch) lines.push(`Suggested remediation: ${redact(patch.summary || 'Patch available.')}`, 'Review the exported patch bundle before applying it.', '')
64
+ }
65
+ lines.push('## Evidence timeline', '')
66
+ for (const e of scan.evidence || []) lines.push(`- [${redact(e.stage)}] **${redact(e.actor)}**: ${redact(e.message)}`)
67
+ return `${lines.join('\n')}\n`
68
+ }
69
+
70
+ async function saveReport(scan) {
71
+ const path = resolve(`patchproof-${scan.id}-report.md`)
72
+ await writeFile(path, makeReport(scan), { mode: 0o600 })
73
+ console.log(`Report written: ${path}`)
74
+ }
75
+
76
+ async function exportFixes(scan) {
77
+ const diffs = (scan.findings || []).flatMap((f) => (f.patches || []).map((p) => p.diff).filter(Boolean))
78
+ if (!diffs.length) {
79
+ console.log('No generated fixes are available for this scan.')
80
+ return
81
+ }
82
+ const path = resolve(`patchproof-${scan.id}.patch`)
83
+ await writeFile(path, `${diffs.join('\n')}\n`, { mode: 0o600 })
84
+ console.log(`Patch bundle written: ${path} · review before applying`)
85
+ }
86
+
87
+ async function scanRepository() {
88
+ const repo = (await terminal.question('Git URL or local repository path> ')).trim()
89
+ if (!repo) return
90
+ let scan = await request('/api/scans', { method: 'POST', body: { repo_url: repo, branch: 'HEAD' } })
91
+ console.log(`Queued scan ${scan.id}`)
92
+ while (!['done', 'failed', 'cancelled'].includes(scan.status)) {
93
+ await sleep(2000)
94
+ scan = await request(`/api/scans/${scan.id}`)
95
+ stdout.write(`\r${scan.status.padEnd(16)}`)
96
+ }
97
+ console.log()
98
+ printScan(scan)
99
+ await saveReport(scan)
100
+ }
101
+
102
+ async function getScan() {
103
+ const id = (await terminal.question('Scan ID> ')).trim()
104
+ if (!/^[a-f\d]{12}$/i.test(id)) throw new Error('Enter a 12-character hexadecimal scan ID.')
105
+ const scan = await request(`/api/scans/${id}`)
106
+ printScan(scan)
107
+ return scan
108
+ }
109
+
110
+ try {
111
+ console.log(`PatchProof Security TUI · ${api}`)
112
+ while (true) {
113
+ console.log('\n1) Scan repository 2) Open report 3) Export fix patch 4) Quit')
114
+ const choice = (await terminal.question('Choose> ')).trim().toLowerCase()
115
+ try {
116
+ if (choice === '1') await scanRepository()
117
+ else if (choice === '2') await saveReport(await getScan())
118
+ else if (choice === '3') await exportFixes(await getScan())
119
+ else if (['4', 'q', 'quit'].includes(choice)) break
120
+ else console.log('Choose 1, 2, 3, or 4.')
121
+ } catch (error) {
122
+ console.error(`Error: ${error.message}`)
123
+ }
124
+ }
125
+ } catch (error) {
126
+ if (error.name !== 'AbortError') console.error(`Error: ${error.message}`)
127
+ } finally {
128
+ terminal.close()
129
+ }
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "name": "patchproof-tui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "PatchProof AI Terminal Interface",
5
+ "main": "bin/patchproof-tui",
6
+ "bin": {
7
+ "patchproof-tui": "bin/patchproof-tui"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "license": "MIT"
14
+ }