praxis-sec 1.0.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 +170 -0
- package/ai-defense/cost-protection.md +292 -0
- package/ai-defense/llm-security-checklist.md +324 -0
- package/ai-defense/prompt-injection-patterns.js +283 -0
- package/ai-defense/system-prompt-armor.md +327 -0
- package/checklists/launch-day.md +168 -0
- package/cli/agents/abom-generator.js +225 -0
- package/cli/agents/agent-attestation-agent.js +318 -0
- package/cli/agents/agent-config-scanner.js +787 -0
- package/cli/agents/agent-telemetry-agent.js +415 -0
- package/cli/agents/agentic-security-agent.js +296 -0
- package/cli/agents/agentic-supply-chain-agent.js +463 -0
- package/cli/agents/ai-infra-inventory-agent.js +449 -0
- package/cli/agents/api-fuzzer.js +345 -0
- package/cli/agents/auth-bypass-agent.js +348 -0
- package/cli/agents/base-agent.js +280 -0
- package/cli/agents/cicd-scanner.js +300 -0
- package/cli/agents/config-auditor.js +757 -0
- package/cli/agents/deep-analyzer.js +776 -0
- package/cli/agents/endpoint-agent-abuse-agent.js +404 -0
- package/cli/agents/exception-handler-agent.js +187 -0
- package/cli/agents/git-history-scanner.js +169 -0
- package/cli/agents/governance-audits.js +138 -0
- package/cli/agents/hermes-security-agent.js +536 -0
- package/cli/agents/html-reporter.js +1125 -0
- package/cli/agents/index.js +147 -0
- package/cli/agents/injection-tester.js +502 -0
- package/cli/agents/legal-risk-agent.js +328 -0
- package/cli/agents/llm-redteam.js +199 -0
- package/cli/agents/managed-agent-scanner.js +333 -0
- package/cli/agents/mcp-security-agent.js +588 -0
- package/cli/agents/memory-poisoning-agent.js +305 -0
- package/cli/agents/mobile-scanner.js +231 -0
- package/cli/agents/model-file-scanner.js +259 -0
- package/cli/agents/orchestrator.js +355 -0
- package/cli/agents/pii-compliance-agent.js +301 -0
- package/cli/agents/policy-engine.js +229 -0
- package/cli/agents/prompt-injection-prober.js +224 -0
- package/cli/agents/rag-security-agent.js +204 -0
- package/cli/agents/recon-agent.js +207 -0
- package/cli/agents/sbom-generator.js +265 -0
- package/cli/agents/scoring-engine.js +273 -0
- package/cli/agents/ssrf-prober.js +130 -0
- package/cli/agents/stateful-watcher.js +238 -0
- package/cli/agents/supabase-rls-agent.js +154 -0
- package/cli/agents/supply-chain-agent.js +857 -0
- package/cli/agents/swarm-orchestrator.js +200 -0
- package/cli/agents/verifier-agent.js +303 -0
- package/cli/agents/vibe-coding-agent.js +250 -0
- package/cli/bin/praxis.js +866 -0
- package/cli/commands/abom.js +73 -0
- package/cli/commands/agent-fix.js +1245 -0
- package/cli/commands/audit.js +1180 -0
- package/cli/commands/autofix.js +383 -0
- package/cli/commands/baseline.js +193 -0
- package/cli/commands/benchmark.js +327 -0
- package/cli/commands/checklist.js +223 -0
- package/cli/commands/ci.js +403 -0
- package/cli/commands/deps.js +516 -0
- package/cli/commands/diff.js +200 -0
- package/cli/commands/doctor.js +195 -0
- package/cli/commands/env-audit.js +349 -0
- package/cli/commands/fix.js +218 -0
- package/cli/commands/guard.js +396 -0
- package/cli/commands/hooks.js +278 -0
- package/cli/commands/init.js +514 -0
- package/cli/commands/legal.js +158 -0
- package/cli/commands/live-advisories.js +241 -0
- package/cli/commands/mcp.js +660 -0
- package/cli/commands/openclaw.js +386 -0
- package/cli/commands/red-team.js +350 -0
- package/cli/commands/redteam.js +78 -0
- package/cli/commands/remediate.js +797 -0
- package/cli/commands/rotate.js +768 -0
- package/cli/commands/rules.js +196 -0
- package/cli/commands/scan-mcp.js +534 -0
- package/cli/commands/scan-skill.js +588 -0
- package/cli/commands/scan-standard.js +251 -0
- package/cli/commands/scan.js +524 -0
- package/cli/commands/score.js +449 -0
- package/cli/commands/shell.js +514 -0
- package/cli/commands/team-report.js +398 -0
- package/cli/commands/undo.js +161 -0
- package/cli/commands/update-intel.js +126 -0
- package/cli/commands/vibe-check.js +276 -0
- package/cli/commands/watch.js +757 -0
- package/cli/commands/web.js +63 -0
- package/cli/core/ast/guardrail-detector.js +141 -0
- package/cli/core/ast/index.js +22 -0
- package/cli/core/ast/parser.js +676 -0
- package/cli/core/ast/scope-tree.js +287 -0
- package/cli/core/ast/taint-tracker.js +158 -0
- package/cli/core/branding.js +37 -0
- package/cli/core/env.js +38 -0
- package/cli/core/errors.js +61 -0
- package/cli/core/fs.js +62 -0
- package/cli/core/output/compliance.js +90 -0
- package/cli/core/output/html-theme.js +158 -0
- package/cli/core/output/index.js +57 -0
- package/cli/core/output/json.js +48 -0
- package/cli/core/output/sarif.js +240 -0
- package/cli/core/version.js +67 -0
- package/cli/core/web/jobs.js +183 -0
- package/cli/core/web/projects.js +146 -0
- package/cli/core/web/server.js +439 -0
- package/cli/data/atlas-knowledge.json +5640 -0
- package/cli/data/eaa-catalog.json +39 -0
- package/cli/data/known-mcps.json +26 -0
- package/cli/data/probes/prompt-injection-corpus.json +271 -0
- package/cli/data/threat-intel.json +85 -0
- package/cli/data/threatpacks/latest.json +41 -0
- package/cli/hooks/patterns.js +313 -0
- package/cli/hooks/post-tool-use.js +140 -0
- package/cli/hooks/pre-tool-use.js +186 -0
- package/cli/index.js +90 -0
- package/cli/providers/llm-provider.js +766 -0
- package/cli/utils/autofix-rules.js +74 -0
- package/cli/utils/cache-manager.js +310 -0
- package/cli/utils/compliance-map.js +191 -0
- package/cli/utils/entropy.js +132 -0
- package/cli/utils/fix-ledger.js +127 -0
- package/cli/utils/hermes-tool-registry.js +252 -0
- package/cli/utils/intel/cache.js +61 -0
- package/cli/utils/intel/http.js +88 -0
- package/cli/utils/intel/index.js +235 -0
- package/cli/utils/intel/merge.js +229 -0
- package/cli/utils/intel/sources/epss.js +54 -0
- package/cli/utils/intel/sources/ghsa.js +81 -0
- package/cli/utils/intel/sources/gitguardian.js +40 -0
- package/cli/utils/intel/sources/gitleaks.js +101 -0
- package/cli/utils/intel/sources/kev.js +38 -0
- package/cli/utils/intel/sources/nvd.js +84 -0
- package/cli/utils/intel/sources/osv.js +132 -0
- package/cli/utils/intel/sources/phylum.js +44 -0
- package/cli/utils/intel/sources/snyk.js +46 -0
- package/cli/utils/intel/sources/socket.js +69 -0
- package/cli/utils/intel/sources/sonatype.js +84 -0
- package/cli/utils/intel/sources/threatpack.js +69 -0
- package/cli/utils/mcp-trust.js +60 -0
- package/cli/utils/output.js +251 -0
- package/cli/utils/patterns.js +1130 -0
- package/cli/utils/pdf-generator.js +94 -0
- package/cli/utils/plugin-loader.js +364 -0
- package/cli/utils/rule-import.js +228 -0
- package/cli/utils/rule-registry.js +426 -0
- package/cli/utils/scan-fingerprint.js +109 -0
- package/cli/utils/scan-playbook.js +312 -0
- package/cli/utils/score-history.js +119 -0
- package/cli/utils/secrets-verifier.js +247 -0
- package/cli/utils/security-memory.js +296 -0
- package/cli/utils/standards/atlas-knowledge.js +87 -0
- package/cli/utils/standards/index.js +127 -0
- package/cli/utils/standards/sources/avid.js +45 -0
- package/cli/utils/standards/sources/eu-ai-act.js +89 -0
- package/cli/utils/standards/sources/google-saif.js +39 -0
- package/cli/utils/standards/sources/iso-42001.js +94 -0
- package/cli/utils/standards/sources/mitre-atlas.js +54 -0
- package/cli/utils/standards/sources/nist-ai-600-1.js +45 -0
- package/cli/utils/standards/sources/owasp-llm.js +45 -0
- package/cli/utils/standards/sources/owasp-ml.js +45 -0
- package/cli/utils/threat-intel.js +265 -0
- package/configs/firebase/firestore-rules.txt +215 -0
- package/configs/firebase/security-checklist.md +236 -0
- package/configs/firebase/storage-rules.txt +206 -0
- package/configs/gitignore-template +258 -0
- package/configs/nextjs-security-headers.js +220 -0
- package/configs/praxisignore-template +50 -0
- package/configs/supabase/secure-client.ts +225 -0
- package/configs/supabase/security-checklist.md +278 -0
- package/docs/THIRD_PARTY_NOTICES.md +26 -0
- package/docs/THREAT_INTEL.md +292 -0
- package/docs/USAGE.md +1205 -0
- package/docs/design/WEB-UI.md +82 -0
- package/package.json +71 -0
- package/scripts/check-determinism.mjs +119 -0
- package/snippets/README.md +122 -0
- package/snippets/api-security/api-security-checklist.md +412 -0
- package/snippets/api-security/cors-config.ts +322 -0
- package/snippets/api-security/input-validation.ts +430 -0
- package/snippets/auth/jwt-checklist.md +322 -0
- package/snippets/rate-limiting/nextjs-middleware.ts +211 -0
- package/snippets/rate-limiting/upstash-ratelimit.ts +229 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autofix Command — Agentic Auto-Fix PRs
|
|
3
|
+
* ========================================
|
|
4
|
+
*
|
|
5
|
+
* Reads findings from a praxis report (or the last audit run), applies the
|
|
6
|
+
* LLM-generated fixes from Tier 3 deep analysis, commits them to a branch, and
|
|
7
|
+
* opens a GitHub pull request.
|
|
8
|
+
*
|
|
9
|
+
* Requires:
|
|
10
|
+
* - A report with deepAnalysis.fix fields (run with `--deep` flag)
|
|
11
|
+
* - Git repository
|
|
12
|
+
* - GitHub CLI (`gh`) installed for PR creation, OR GITHUB_TOKEN + repo info
|
|
13
|
+
*
|
|
14
|
+
* USAGE:
|
|
15
|
+
* npx praxis-sec autofix Auto-fix from last report
|
|
16
|
+
* npx praxis-sec autofix --report report.json Auto-fix from specific report
|
|
17
|
+
* npx praxis-sec autofix --dry-run Preview fixes without applying
|
|
18
|
+
* npx praxis-sec autofix --severity high Only fix critical+high findings
|
|
19
|
+
*
|
|
20
|
+
* SAFETY:
|
|
21
|
+
* - Never auto-commits secrets, config files, or .env
|
|
22
|
+
* - Always creates a new branch (never pushes to main/master/develop)
|
|
23
|
+
* - Dry-run mode shows a diff without writing any files
|
|
24
|
+
* - Each fix is applied atomically — if a file fails, others continue
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'fs';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
import { execFileSync, execSync } from 'child_process';
|
|
30
|
+
import chalk from 'chalk';
|
|
31
|
+
import * as output from '../utils/output.js';
|
|
32
|
+
|
|
33
|
+
// Severity rank for filtering
|
|
34
|
+
const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
35
|
+
|
|
36
|
+
// Files we never auto-edit (secrets, config, generated)
|
|
37
|
+
const NEVER_EDIT = [
|
|
38
|
+
/\.env(\.|$)/i,
|
|
39
|
+
/\.pem$|\.key$|\.p12$|\.pfx$/i,
|
|
40
|
+
/package-lock\.json$|yarn\.lock$|pnpm-lock\.yaml$/i,
|
|
41
|
+
/\.min\.(js|css)$/,
|
|
42
|
+
/node_modules\//,
|
|
43
|
+
/dist\//,
|
|
44
|
+
/build\//,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// MAIN
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
export async function autofixCommand(options = {}) {
|
|
52
|
+
const rootPath = path.resolve(options.path || '.');
|
|
53
|
+
const dryRun = options.dryRun || false;
|
|
54
|
+
const minSev = options.severity || 'high';
|
|
55
|
+
const minRank = SEV_RANK[minSev] ?? 3;
|
|
56
|
+
const reportPath = options.report
|
|
57
|
+
? path.resolve(options.report)
|
|
58
|
+
: findLastReport(rootPath);
|
|
59
|
+
|
|
60
|
+
console.log();
|
|
61
|
+
output.header('Praxis — Agentic Autofix');
|
|
62
|
+
console.log();
|
|
63
|
+
|
|
64
|
+
if (!reportPath || !fs.existsSync(reportPath)) {
|
|
65
|
+
output.error('No report found. Run `npx praxis-sec audit . --deep --json` first, or pass --report <path>.');
|
|
66
|
+
console.log(chalk.gray(' The --deep flag enables Tier 3 exploit-chain analysis that generates fix suggestions.'));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Load Report ─────────────────────────────────────────────────────────
|
|
71
|
+
let report;
|
|
72
|
+
try {
|
|
73
|
+
report = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
|
|
74
|
+
} catch (err) {
|
|
75
|
+
output.error(`Failed to parse report: ${err.message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const findings = report.findings ?? [];
|
|
80
|
+
console.log(chalk.gray(` Report: ${reportPath}`));
|
|
81
|
+
console.log(chalk.gray(` Total findings: ${findings.length}`));
|
|
82
|
+
console.log();
|
|
83
|
+
|
|
84
|
+
// ── Filter to fixable findings ──────────────────────────────────────────
|
|
85
|
+
const fixable = findings.filter(f => {
|
|
86
|
+
if (!f.deepAnalysis?.fix) return false;
|
|
87
|
+
if ((SEV_RANK[f.severity] ?? 0) < minRank) return false;
|
|
88
|
+
if (!f.file) return false;
|
|
89
|
+
const absFile = path.resolve(rootPath, f.file);
|
|
90
|
+
if (NEVER_EDIT.some(p => p.test(absFile.replace(/\\/g, '/')))) return false;
|
|
91
|
+
if (!fs.existsSync(absFile)) return false;
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (fixable.length === 0) {
|
|
96
|
+
console.log(chalk.yellow(` No fixable findings found at severity >= ${minSev}.`));
|
|
97
|
+
console.log(chalk.gray(' Tip: Run `npx praxis-sec audit . --deep` to generate AI-powered fix suggestions.'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(chalk.cyan(` Found ${fixable.length} fixable finding(s) at severity >= ${minSev}:`));
|
|
102
|
+
console.log();
|
|
103
|
+
|
|
104
|
+
for (const f of fixable) {
|
|
105
|
+
const sev = f.severity === 'critical' ? chalk.red.bold(f.severity)
|
|
106
|
+
: f.severity === 'high' ? chalk.yellow(f.severity)
|
|
107
|
+
: chalk.blue(f.severity);
|
|
108
|
+
console.log(` ${sev} ${chalk.white(f.title)}`);
|
|
109
|
+
console.log(` ${chalk.gray('File:')} ${f.file}${f.line ? `:${f.line}` : ''}`);
|
|
110
|
+
console.log(` ${chalk.gray('Fix:')} ${f.deepAnalysis.fix}`);
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (dryRun) {
|
|
115
|
+
console.log(chalk.yellow(' Dry-run mode — no files will be changed.'));
|
|
116
|
+
console.log(chalk.gray(' Remove --dry-run to apply fixes and open a PR.'));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Confirm ──────────────────────────────────────────────────────────────
|
|
121
|
+
if (!options.yes) {
|
|
122
|
+
const { createInterface } = await import('readline');
|
|
123
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
124
|
+
const answer = await new Promise(resolve => {
|
|
125
|
+
rl.question(chalk.cyan(` Apply ${fixable.length} fix(es) and open a PR? [y/N] `), resolve);
|
|
126
|
+
});
|
|
127
|
+
rl.close();
|
|
128
|
+
if (!/^y/i.test(answer)) {
|
|
129
|
+
console.log(chalk.gray('\n Cancelled.\n'));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Check git state ──────────────────────────────────────────────────────
|
|
136
|
+
if (!isGitRepo(rootPath)) {
|
|
137
|
+
output.error('Not a git repository. Autofix requires git.');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const currentBranch = getCurrentBranch(rootPath);
|
|
142
|
+
const protectedBranches = ['main', 'master', 'develop', 'dev', 'production', 'staging'];
|
|
143
|
+
if (protectedBranches.includes(currentBranch)) {
|
|
144
|
+
// Work on a new branch instead
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
148
|
+
const branchName = `praxis/autofix-${timestamp}`;
|
|
149
|
+
|
|
150
|
+
console.log(chalk.gray(` Creating branch: ${branchName}`));
|
|
151
|
+
try {
|
|
152
|
+
execFileSync('git', ['checkout', '-b', branchName], { cwd: rootPath, stdio: 'pipe' });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
output.error(`Failed to create branch: ${err.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Apply fixes ──────────────────────────────────────────────────────────
|
|
159
|
+
const applied = [];
|
|
160
|
+
const failed = [];
|
|
161
|
+
|
|
162
|
+
for (const f of fixable) {
|
|
163
|
+
const absFile = path.resolve(rootPath, f.file);
|
|
164
|
+
const fix = f.deepAnalysis.fix;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
applyInlineAnnotation(absFile, f.line, fix);
|
|
168
|
+
applied.push(f);
|
|
169
|
+
console.log(chalk.green(` ✔ Annotated: ${f.file}:${f.line ?? ''}`));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
failed.push({ finding: f, error: err.message });
|
|
172
|
+
console.log(chalk.yellow(` ⚠ Skipped: ${f.file} — ${err.message}`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (applied.length === 0) {
|
|
177
|
+
console.log(chalk.yellow('\n No files were changed. Cleaning up branch...'));
|
|
178
|
+
try {
|
|
179
|
+
execFileSync('git', ['checkout', currentBranch], { cwd: rootPath, stdio: 'pipe' });
|
|
180
|
+
execFileSync('git', ['branch', '-D', branchName], { cwd: rootPath, stdio: 'pipe' });
|
|
181
|
+
} catch { /* ignore cleanup errors */ }
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Commit ───────────────────────────────────────────────────────────────
|
|
186
|
+
console.log(chalk.gray(`\n Committing ${applied.length} fix annotation(s)...`));
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const filesToStage = [...new Set(applied.map(f => path.resolve(rootPath, f.file)))];
|
|
190
|
+
execFileSync('git', ['add', ...filesToStage], { cwd: rootPath, stdio: 'pipe' });
|
|
191
|
+
|
|
192
|
+
const commitMsg = [
|
|
193
|
+
`fix(security): apply praxis autofix annotations`,
|
|
194
|
+
'',
|
|
195
|
+
`Addresses ${applied.length} finding(s) from praxis audit:`,
|
|
196
|
+
...applied.map(f => `- ${f.severity.toUpperCase()}: ${f.title} (${f.file}${f.line ? `:${f.line}` : ''})`),
|
|
197
|
+
'',
|
|
198
|
+
'Fix suggestions generated by praxis Tier 3 (Opus) deep analysis.',
|
|
199
|
+
'Review each annotation and apply the suggested code change.',
|
|
200
|
+
].join('\n');
|
|
201
|
+
|
|
202
|
+
execFileSync('git', ['commit', '-m', commitMsg], { cwd: rootPath, stdio: 'pipe' });
|
|
203
|
+
console.log(chalk.green(' Committed.'));
|
|
204
|
+
} catch (err) {
|
|
205
|
+
output.error(`Commit failed: ${err.message}`);
|
|
206
|
+
// Restore branch state
|
|
207
|
+
try { execFileSync('git', ['checkout', currentBranch], { cwd: rootPath, stdio: 'pipe' }); } catch { /* ignore */ }
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Push and open PR ─────────────────────────────────────────────────────
|
|
212
|
+
console.log(chalk.gray(' Pushing branch...'));
|
|
213
|
+
try {
|
|
214
|
+
execFileSync('git', ['push', '-u', 'origin', branchName], { cwd: rootPath, stdio: 'pipe' });
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.log(chalk.yellow(` Push failed: ${err.message}`));
|
|
217
|
+
console.log(chalk.gray(` Branch created locally: ${branchName}`));
|
|
218
|
+
console.log(chalk.gray(' Push manually with: git push -u origin ' + branchName));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── Open PR ───────────────────────────────────────────────────────────────
|
|
223
|
+
const prBody = buildPRBody(applied, failed, reportPath);
|
|
224
|
+
const prTitle = `fix(security): praxis autofix — ${applied.length} finding(s)`;
|
|
225
|
+
|
|
226
|
+
let prUrl = null;
|
|
227
|
+
const ghAvailable = isCommandAvailable('gh');
|
|
228
|
+
|
|
229
|
+
if (ghAvailable) {
|
|
230
|
+
try {
|
|
231
|
+
const result = execFileSync('gh', [
|
|
232
|
+
'pr', 'create',
|
|
233
|
+
'--title', prTitle,
|
|
234
|
+
'--body', prBody,
|
|
235
|
+
'--base', currentBranch,
|
|
236
|
+
'--head', branchName,
|
|
237
|
+
], { cwd: rootPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
238
|
+
prUrl = result.trim();
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.log(chalk.yellow(` gh pr create failed: ${err.stderr?.toString().trim() || err.message}`));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
245
|
+
console.log();
|
|
246
|
+
console.log(chalk.green.bold(` ✔ Autofix complete`));
|
|
247
|
+
console.log(` Applied: ${chalk.white(applied.length)} fix annotation(s)`);
|
|
248
|
+
if (failed.length > 0) console.log(` Skipped: ${chalk.yellow(failed.length)} (see above)`);
|
|
249
|
+
console.log(` Branch: ${chalk.cyan(branchName)}`);
|
|
250
|
+
if (prUrl) {
|
|
251
|
+
console.log(` PR: ${chalk.cyan(prUrl)}`);
|
|
252
|
+
} else {
|
|
253
|
+
console.log(chalk.gray(' Install `gh` (GitHub CLI) to auto-open pull requests.'));
|
|
254
|
+
}
|
|
255
|
+
console.log();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// =============================================================================
|
|
259
|
+
// HELPERS
|
|
260
|
+
// =============================================================================
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Apply the fix as a structured comment annotation above the finding line.
|
|
264
|
+
* The comment explains the issue and the fix — a developer reviews and applies.
|
|
265
|
+
* We never blindly rewrite code; instead we annotate so engineers make the call.
|
|
266
|
+
*/
|
|
267
|
+
/**
|
|
268
|
+
* Apply fix annotations to a list of findings in-place.
|
|
269
|
+
* Returns the count of files successfully annotated.
|
|
270
|
+
* Exported for use by the --agentic audit loop.
|
|
271
|
+
*/
|
|
272
|
+
export function applyInlineAnnotations(findings) {
|
|
273
|
+
const NEVER_EDIT = new Set(['.env', '.env.local', '.env.production', 'secrets.json', '.npmrc', '.netrc']);
|
|
274
|
+
const fixable = findings.filter(f =>
|
|
275
|
+
f.fix && f.file && fs.existsSync(f.file) && !NEVER_EDIT.has(path.basename(f.file))
|
|
276
|
+
);
|
|
277
|
+
let count = 0;
|
|
278
|
+
for (const f of fixable.slice(0, 10)) {
|
|
279
|
+
try {
|
|
280
|
+
applyInlineAnnotation(f.file, f.line, f.fix);
|
|
281
|
+
count++;
|
|
282
|
+
} catch { /* skip unwritable */ }
|
|
283
|
+
}
|
|
284
|
+
return count;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function applyInlineAnnotation(filePath, lineNum, fix) {
|
|
288
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
289
|
+
const lines = content.split('\n');
|
|
290
|
+
const idx = Math.max(0, (lineNum || 1) - 1);
|
|
291
|
+
|
|
292
|
+
if (idx >= lines.length) {
|
|
293
|
+
throw new Error(`Line ${lineNum} out of range`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Already annotated?
|
|
297
|
+
if (idx > 0 && /praxis-fix/i.test(lines[idx - 1])) return;
|
|
298
|
+
|
|
299
|
+
const indent = lines[idx].match(/^(\s*)/)?.[1] ?? '';
|
|
300
|
+
const isJs = /\.(js|ts|jsx|tsx|mjs|cjs|java|c|cpp|cs|go|rs|swift|kt)$/.test(filePath);
|
|
301
|
+
const isPy = /\.py$/.test(filePath);
|
|
302
|
+
|
|
303
|
+
// Wrap fix in a structured annotation comment
|
|
304
|
+
const fixLines = fix.split('\n').map(l => l.trim()).filter(Boolean);
|
|
305
|
+
let annotation;
|
|
306
|
+
|
|
307
|
+
if (isPy) {
|
|
308
|
+
annotation = [
|
|
309
|
+
`${indent}# praxis-fix [REVIEW REQUIRED]`,
|
|
310
|
+
...fixLines.map(l => `${indent}# ${l}`),
|
|
311
|
+
].join('\n');
|
|
312
|
+
} else {
|
|
313
|
+
annotation = [
|
|
314
|
+
`${indent}// praxis-fix [REVIEW REQUIRED]`,
|
|
315
|
+
...fixLines.map(l => `${indent}// ${l}`),
|
|
316
|
+
].join('\n');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
lines.splice(idx, 0, annotation);
|
|
320
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildPRBody(applied, failed, reportPath) {
|
|
324
|
+
const rows = applied.map(f =>
|
|
325
|
+
`| ${f.severity} | ${f.title} | \`${f.file}${f.line ? `:${f.line}` : ''}\` | ${f.deepAnalysis.fix?.slice(0, 80) ?? ''} |`
|
|
326
|
+
).join('\n');
|
|
327
|
+
|
|
328
|
+
return `## Security Autofix
|
|
329
|
+
|
|
330
|
+
Praxis detected **${applied.length}** security finding(s) and has annotated the affected files with fix instructions. Each annotation is marked \`// praxis-fix [REVIEW REQUIRED]\` — **please review and apply the suggested changes before merging.**
|
|
331
|
+
|
|
332
|
+
### Findings Fixed
|
|
333
|
+
|
|
334
|
+
| Severity | Title | Location | Fix Summary |
|
|
335
|
+
|----------|-------|----------|-------------|
|
|
336
|
+
${rows}
|
|
337
|
+
|
|
338
|
+
${failed.length > 0 ? `### Skipped (${failed.length})\n\n${failed.map(f => `- ${f.finding.title}: ${f.error}`).join('\n')}` : ''}
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
> Generated by [praxis]() — AI-powered security scanner.
|
|
343
|
+
> Run \`npx praxis-sec audit . --deep\` to regenerate findings.
|
|
344
|
+
|
|
345
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function findLastReport(rootPath) {
|
|
349
|
+
// Look for common report filenames in order of preference
|
|
350
|
+
const candidates = [
|
|
351
|
+
'praxis-report.json',
|
|
352
|
+
'.praxis/last-report.json',
|
|
353
|
+
'security-report.json',
|
|
354
|
+
].map(f => path.join(rootPath, f));
|
|
355
|
+
|
|
356
|
+
return candidates.find(p => fs.existsSync(p)) ?? null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isGitRepo(rootPath) {
|
|
360
|
+
try {
|
|
361
|
+
execFileSync('git', ['rev-parse', '--git-dir'], { cwd: rootPath, stdio: 'pipe' });
|
|
362
|
+
return true;
|
|
363
|
+
} catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function getCurrentBranch(rootPath) {
|
|
369
|
+
try {
|
|
370
|
+
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: rootPath, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
371
|
+
} catch {
|
|
372
|
+
return 'main';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isCommandAvailable(cmd) {
|
|
377
|
+
try {
|
|
378
|
+
execFileSync(cmd, ['--version'], { stdio: 'pipe' });
|
|
379
|
+
return true;
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline Command
|
|
3
|
+
* =================
|
|
4
|
+
*
|
|
5
|
+
* Accept current findings as a baseline and only report new findings
|
|
6
|
+
* on subsequent scans. This is how teams adopt security scanners:
|
|
7
|
+
* baseline existing debt, focus on not making it worse.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* praxis baseline . Create a new baseline from current scan
|
|
11
|
+
* praxis baseline . --diff Show what changed since baseline
|
|
12
|
+
* praxis baseline . --clear Remove the baseline
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import ora from 'ora';
|
|
19
|
+
import { buildOrchestrator } from '../agents/index.js';
|
|
20
|
+
import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, MAX_FILE_SIZE } from '../utils/patterns.js';
|
|
21
|
+
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
22
|
+
import fg from 'fast-glob';
|
|
23
|
+
|
|
24
|
+
const BASELINE_FILE = '.praxis/baseline.json';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a fingerprint for a finding that survives line-number shifts.
|
|
28
|
+
* Uses rule + relative file path + first 40 chars of matched text.
|
|
29
|
+
*/
|
|
30
|
+
function fingerprint(finding, rootPath) {
|
|
31
|
+
const relFile = path.relative(rootPath, finding.file || '').replace(/\\/g, '/');
|
|
32
|
+
const matched = (finding.matched || '').slice(0, 40);
|
|
33
|
+
return `${finding.rule}:${relFile}:${matched}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Quick secret scan (same as audit Phase 1) to get current findings.
|
|
38
|
+
*/
|
|
39
|
+
async function quickScan(rootPath) {
|
|
40
|
+
const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
|
|
41
|
+
const files = await fg('**/*', {
|
|
42
|
+
cwd: rootPath, absolute: true, onlyFiles: true, ignore: globIgnore, dot: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const filtered = files.filter(f => {
|
|
46
|
+
const ext = path.extname(f).toLowerCase();
|
|
47
|
+
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
48
|
+
if (SKIP_FILENAMES.has(path.basename(f))) return false;
|
|
49
|
+
try { return fs.statSync(f).size <= MAX_FILE_SIZE; } catch { return false; }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const findings = [];
|
|
53
|
+
for (const file of filtered) {
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
56
|
+
const lines = content.split('\n');
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
if (/praxis-ignore/i.test(lines[i])) continue;
|
|
59
|
+
for (const p of SECRET_PATTERNS) {
|
|
60
|
+
p.pattern.lastIndex = 0;
|
|
61
|
+
let m;
|
|
62
|
+
while ((m = p.pattern.exec(lines[i])) !== null) {
|
|
63
|
+
if (p.requiresEntropyCheck && !isHighEntropyMatch(m[0])) continue;
|
|
64
|
+
findings.push({ file, line: i + 1, rule: p.name, matched: m[0], severity: p.severity });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch { /* skip */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { findings, files: filtered };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run agents and combine with secret scan findings.
|
|
76
|
+
*/
|
|
77
|
+
async function fullScan(rootPath) {
|
|
78
|
+
const { findings: secretFindings, files } = await quickScan(rootPath);
|
|
79
|
+
|
|
80
|
+
const orchestrator = buildOrchestrator();
|
|
81
|
+
const { findings: agentFindings } = await orchestrator.runAll(rootPath, { quiet: true }); // praxis-ignore — orchestrator result, not LLM output triggering actions
|
|
82
|
+
|
|
83
|
+
return [...secretFindings, ...agentFindings];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Load existing baseline from disk.
|
|
88
|
+
*/
|
|
89
|
+
function loadBaseline(rootPath) {
|
|
90
|
+
const baselinePath = path.join(rootPath, BASELINE_FILE);
|
|
91
|
+
if (!fs.existsSync(baselinePath)) return null;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Save baseline to disk.
|
|
101
|
+
*/
|
|
102
|
+
function saveBaseline(rootPath, fingerprints, findingCount) {
|
|
103
|
+
const baselinePath = path.join(rootPath, BASELINE_FILE);
|
|
104
|
+
const dir = path.dirname(baselinePath);
|
|
105
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
106
|
+
|
|
107
|
+
const baseline = {
|
|
108
|
+
version: '4.3.0',
|
|
109
|
+
createdAt: new Date().toISOString(),
|
|
110
|
+
fingerprints,
|
|
111
|
+
findingCount,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
fs.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2));
|
|
115
|
+
return baseline;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Filter out baselined findings. Returns only new findings.
|
|
120
|
+
*/
|
|
121
|
+
export function filterBaseline(findings, rootPath) {
|
|
122
|
+
const baseline = loadBaseline(rootPath);
|
|
123
|
+
if (!baseline) return findings;
|
|
124
|
+
|
|
125
|
+
const baseSet = new Set(baseline.fingerprints);
|
|
126
|
+
return findings.filter(f => !baseSet.has(fingerprint(f, rootPath)));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Main baseline command.
|
|
131
|
+
*/
|
|
132
|
+
export async function baselineCommand(targetPath = '.', options = {}) {
|
|
133
|
+
const rootPath = path.resolve(targetPath);
|
|
134
|
+
|
|
135
|
+
if (options.clear) {
|
|
136
|
+
const baselinePath = path.join(rootPath, BASELINE_FILE);
|
|
137
|
+
if (fs.existsSync(baselinePath)) {
|
|
138
|
+
fs.unlinkSync(baselinePath);
|
|
139
|
+
console.log(chalk.green(' Baseline removed.'));
|
|
140
|
+
} else {
|
|
141
|
+
console.log(chalk.gray(' No baseline found.'));
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (options.diff) {
|
|
147
|
+
const baseline = loadBaseline(rootPath);
|
|
148
|
+
if (!baseline) {
|
|
149
|
+
console.log(chalk.yellow(' No baseline found. Run `praxis baseline .` first.'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const spinner = ora({ text: 'Scanning for comparison...', color: 'cyan' }).start();
|
|
154
|
+
const findings = await fullScan(rootPath);
|
|
155
|
+
spinner.stop();
|
|
156
|
+
|
|
157
|
+
const currentFingerprints = new Set(findings.map(f => fingerprint(f, rootPath)));
|
|
158
|
+
const baseSet = new Set(baseline.fingerprints);
|
|
159
|
+
|
|
160
|
+
const newFindings = findings.filter(f => !baseSet.has(fingerprint(f, rootPath)));
|
|
161
|
+
const resolvedCount = baseline.fingerprints.filter(fp => !currentFingerprints.has(fp)).length;
|
|
162
|
+
|
|
163
|
+
console.log(chalk.cyan.bold('\n Baseline Comparison'));
|
|
164
|
+
console.log(chalk.gray(` Baseline: ${baseline.findingCount} findings (${baseline.createdAt.slice(0, 10)})`));
|
|
165
|
+
console.log(chalk.gray(` Current: ${findings.length} findings`));
|
|
166
|
+
console.log();
|
|
167
|
+
if (newFindings.length > 0) {
|
|
168
|
+
console.log(chalk.red(` + ${newFindings.length} new finding(s)`));
|
|
169
|
+
}
|
|
170
|
+
if (resolvedCount > 0) {
|
|
171
|
+
console.log(chalk.green(` - ${resolvedCount} resolved finding(s)`));
|
|
172
|
+
}
|
|
173
|
+
if (newFindings.length === 0 && resolvedCount === 0) {
|
|
174
|
+
console.log(chalk.green(' No changes since baseline.'));
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Default: create/update baseline
|
|
180
|
+
const spinner = ora({ text: 'Running full scan for baseline...', color: 'cyan' }).start();
|
|
181
|
+
const findings = await fullScan(rootPath);
|
|
182
|
+
spinner.stop();
|
|
183
|
+
|
|
184
|
+
const fingerprints = [...new Set(findings.map(f => fingerprint(f, rootPath)))];
|
|
185
|
+
const baseline = saveBaseline(rootPath, fingerprints, findings.length);
|
|
186
|
+
|
|
187
|
+
console.log(chalk.green.bold('\n Baseline created'));
|
|
188
|
+
console.log(chalk.gray(` Findings baselined: ${findings.length}`));
|
|
189
|
+
console.log(chalk.gray(` Unique fingerprints: ${fingerprints.length}`));
|
|
190
|
+
console.log(chalk.gray(` Saved to: ${BASELINE_FILE}`));
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(chalk.gray(' Run `praxis audit . --baseline` to only see new findings.'));
|
|
193
|
+
}
|