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,757 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch Command
|
|
3
|
+
* ==============
|
|
4
|
+
*
|
|
5
|
+
* Continuous file monitoring mode. Watches for file changes
|
|
6
|
+
* and incrementally scans modified files.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* npx praxis-sec watch [path] Start watching for changes
|
|
10
|
+
* npx praxis-sec watch . --poll Use polling (for network drives)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { execFileSync } from 'child_process';
|
|
17
|
+
import { SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, SECRET_PATTERNS, SECURITY_PATTERNS } from '../utils/patterns.js';
|
|
18
|
+
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
19
|
+
import * as output from '../utils/output.js';
|
|
20
|
+
import { ScoringEngine } from '../agents/scoring-engine.js';
|
|
21
|
+
|
|
22
|
+
// Agent config files to watch
|
|
23
|
+
const AGENT_CONFIG_PATTERNS = [
|
|
24
|
+
'.cursorrules', '.windsurfrules', 'CLAUDE.md', 'AGENTS.md',
|
|
25
|
+
'.github/copilot-instructions.md', '.aider.conf.yml',
|
|
26
|
+
'.continue/config.json', 'openclaw.json', 'openclaw.config.json',
|
|
27
|
+
'clawhub.json', 'mcp.json', '.claude/settings.json',
|
|
28
|
+
'.cursor/mcp.json', '.vscode/mcp.json',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Watch state persistence
|
|
32
|
+
const WATCH_DB_DIR = '.praxis';
|
|
33
|
+
const WATCH_DB_FILE = 'watch.json';
|
|
34
|
+
|
|
35
|
+
export async function watchCommand(targetPath = '.', options = {}) {
|
|
36
|
+
const absolutePath = path.resolve(targetPath);
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(absolutePath)) {
|
|
39
|
+
output.error(`Path does not exist: ${absolutePath}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Status mode: print current watch state and exit
|
|
44
|
+
if (options.status) {
|
|
45
|
+
return showWatchStatus(absolutePath);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Config-only watch mode
|
|
49
|
+
if (options.configs) {
|
|
50
|
+
return watchConfigs(absolutePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Stateful mode: persistent K2.6 session (subset of deep)
|
|
54
|
+
if (options.stateful) {
|
|
55
|
+
return watchStateful(absolutePath, options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Deep mode: run full orchestrator on changes
|
|
59
|
+
if (options.deep) {
|
|
60
|
+
return watchDeep(absolutePath, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log();
|
|
64
|
+
output.header('Praxis — Watch Mode');
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(chalk.cyan(' Watching for file changes...'));
|
|
67
|
+
console.log(chalk.gray(' Use --deep for full agent scanning, --status for current findings'));
|
|
68
|
+
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
69
|
+
console.log();
|
|
70
|
+
|
|
71
|
+
const allPatterns = [...SECRET_PATTERNS, ...SECURITY_PATTERNS];
|
|
72
|
+
const skipDirSet = SKIP_DIRS;
|
|
73
|
+
let debounceTimer = null;
|
|
74
|
+
const pendingFiles = new Set();
|
|
75
|
+
|
|
76
|
+
// Use fs.watch recursively
|
|
77
|
+
try {
|
|
78
|
+
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => { // praxis-ignore — filename from fs.watch OS event, not user input
|
|
79
|
+
if (!filename) return; // praxis-ignore
|
|
80
|
+
|
|
81
|
+
const fullPath = path.join(absolutePath, filename); // praxis-ignore API_PATH_IN_FILENAME — filename from fs.watch OS events, path.join with absolute base // praxis-ignore — filename from fs.watch, not user input
|
|
82
|
+
const relPath = filename.replace(/\\/g, '/');
|
|
83
|
+
|
|
84
|
+
// Skip directories we don't care about
|
|
85
|
+
for (const skipDir of skipDirSet) {
|
|
86
|
+
if (relPath.includes(`${skipDir}/`) || relPath.startsWith(`${skipDir}/`)) return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Skip non-code files
|
|
90
|
+
const ext = path.extname(filename).toLowerCase(); // praxis-ignore — filename from fs.watch OS event
|
|
91
|
+
if (SKIP_EXTENSIONS.has(ext)) return;
|
|
92
|
+
if (SKIP_FILENAMES.has(path.basename(filename))) return; // praxis-ignore
|
|
93
|
+
if (filename.endsWith('.min.js') || filename.endsWith('.min.css')) return;
|
|
94
|
+
|
|
95
|
+
// Add to pending and debounce
|
|
96
|
+
pendingFiles.add(fullPath);
|
|
97
|
+
|
|
98
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
99
|
+
debounceTimer = setTimeout(() => {
|
|
100
|
+
const filesToScan = [...pendingFiles];
|
|
101
|
+
pendingFiles.clear();
|
|
102
|
+
scanChangedFiles(filesToScan, allPatterns, absolutePath);
|
|
103
|
+
}, 300);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Keep the process alive
|
|
107
|
+
process.on('SIGINT', () => {
|
|
108
|
+
watcher.close();
|
|
109
|
+
console.log();
|
|
110
|
+
output.info('Watch mode stopped.');
|
|
111
|
+
process.exit(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Prevent Node from exiting
|
|
115
|
+
setInterval(() => {}, 1000 * 60 * 60);
|
|
116
|
+
|
|
117
|
+
} catch (err) {
|
|
118
|
+
output.error(`Watch failed: ${err.message}`);
|
|
119
|
+
console.log(chalk.gray(' Try: npx praxis-sec watch . --poll'));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function scanChangedFiles(files, patterns, rootPath) {
|
|
125
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
126
|
+
let totalFindings = 0;
|
|
127
|
+
|
|
128
|
+
for (const filePath of files) {
|
|
129
|
+
if (!fs.existsSync(filePath)) continue;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const stats = fs.statSync(filePath);
|
|
133
|
+
if (stats.size > 1_000_000) continue;
|
|
134
|
+
} catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const findings = scanFile(filePath, patterns);
|
|
139
|
+
if (findings.length > 0) {
|
|
140
|
+
totalFindings += findings.length;
|
|
141
|
+
const relPath = path.relative(rootPath, filePath);
|
|
142
|
+
|
|
143
|
+
for (const f of findings) {
|
|
144
|
+
const sevColor = f.severity === 'critical' ? chalk.red.bold
|
|
145
|
+
: f.severity === 'high' ? chalk.yellow
|
|
146
|
+
: chalk.blue;
|
|
147
|
+
|
|
148
|
+
console.log(
|
|
149
|
+
chalk.gray(` [${timestamp}] `) +
|
|
150
|
+
sevColor(`[${f.severity.toUpperCase()}]`) +
|
|
151
|
+
chalk.white(` ${relPath}:${f.line} `) +
|
|
152
|
+
chalk.gray(f.patternName)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (totalFindings === 0 && files.length > 0) {
|
|
159
|
+
console.log(chalk.gray(` [${timestamp}] ${files.length} file(s) scanned — clean`));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function scanFile(filePath, patterns) {
|
|
164
|
+
const findings = [];
|
|
165
|
+
try {
|
|
166
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
167
|
+
const lines = content.split('\n');
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const line = lines[i];
|
|
171
|
+
if (/praxis-ignore/i.test(line)) continue;
|
|
172
|
+
|
|
173
|
+
for (const pattern of patterns) {
|
|
174
|
+
pattern.pattern.lastIndex = 0;
|
|
175
|
+
let match;
|
|
176
|
+
while ((match = pattern.pattern.exec(line)) !== null) {
|
|
177
|
+
if (pattern.requiresEntropyCheck && !isHighEntropyMatch(match[0])) continue;
|
|
178
|
+
findings.push({
|
|
179
|
+
line: i + 1,
|
|
180
|
+
patternName: pattern.name,
|
|
181
|
+
severity: pattern.severity,
|
|
182
|
+
matched: match[0],
|
|
183
|
+
category: pattern.category || 'secret',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch { /* skip */ }
|
|
189
|
+
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
return findings.filter(f => {
|
|
192
|
+
const key = `${f.line}:${f.matched}`;
|
|
193
|
+
if (seen.has(key)) return false;
|
|
194
|
+
seen.add(key);
|
|
195
|
+
return true;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// =============================================================================
|
|
200
|
+
// CONFIG-ONLY WATCH MODE
|
|
201
|
+
// =============================================================================
|
|
202
|
+
|
|
203
|
+
async function watchConfigs(absolutePath) {
|
|
204
|
+
console.log();
|
|
205
|
+
output.header('Praxis — Agent Config Watch');
|
|
206
|
+
console.log();
|
|
207
|
+
console.log(chalk.cyan(' Watching agent config files for changes...'));
|
|
208
|
+
console.log(chalk.gray(' Monitors: .cursorrules, CLAUDE.md, openclaw.json, mcp.json, .claude/settings.json, ...'));
|
|
209
|
+
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
210
|
+
console.log();
|
|
211
|
+
|
|
212
|
+
let debounceTimer = null;
|
|
213
|
+
const pendingFiles = new Set();
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
217
|
+
if (!filename) return;
|
|
218
|
+
|
|
219
|
+
// Check if this is an agent config file
|
|
220
|
+
const relPath = filename.replace(/\\/g, '/');
|
|
221
|
+
const isConfig = AGENT_CONFIG_PATTERNS.some(p => relPath === p || relPath.endsWith('/' + p));
|
|
222
|
+
const isGlobMatch = relPath.match(/\.cursor\/rules\/.*\.mdc$/) ||
|
|
223
|
+
relPath.match(/\.openclaw\/.*\.json$/) ||
|
|
224
|
+
relPath.match(/\.claude\/commands\/.*\.md$/) ||
|
|
225
|
+
relPath.match(/\.claude\/memory\//);
|
|
226
|
+
|
|
227
|
+
if (!isConfig && !isGlobMatch) return;
|
|
228
|
+
|
|
229
|
+
const fullPath = path.join(absolutePath, filename); // praxis-ignore API_PATH_IN_FILENAME — filename from fs.watch OS events, path.join with absolute base
|
|
230
|
+
pendingFiles.add(fullPath);
|
|
231
|
+
|
|
232
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
233
|
+
debounceTimer = setTimeout(async () => {
|
|
234
|
+
const filesToScan = [...pendingFiles];
|
|
235
|
+
pendingFiles.clear();
|
|
236
|
+
await scanConfigFiles(filesToScan, absolutePath);
|
|
237
|
+
}, 300);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
process.on('SIGINT', () => {
|
|
241
|
+
watcher.close();
|
|
242
|
+
console.log();
|
|
243
|
+
output.info('Config watch stopped.');
|
|
244
|
+
process.exit(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
setInterval(() => {}, 1000 * 60 * 60);
|
|
248
|
+
|
|
249
|
+
} catch (err) {
|
|
250
|
+
output.error(`Watch failed: ${err.message}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// =============================================================================
|
|
256
|
+
// STATUS MODE
|
|
257
|
+
// =============================================================================
|
|
258
|
+
|
|
259
|
+
function showWatchStatus(rootPath) {
|
|
260
|
+
const dbFile = path.join(rootPath, WATCH_DB_DIR, WATCH_DB_FILE);
|
|
261
|
+
if (!fs.existsSync(dbFile)) {
|
|
262
|
+
console.log('\n No watch data found. Run: praxis watch . --deep\n');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const data = JSON.parse(fs.readFileSync(dbFile, 'utf-8'));
|
|
268
|
+
console.log(`\n ${chalk.cyan.bold('Praxis Watch — Status')}`);
|
|
269
|
+
console.log(` ${'─'.repeat(40)}`);
|
|
270
|
+
console.log(` Last scan: ${data.lastScan || 'never'}`);
|
|
271
|
+
console.log(` Scans run: ${data.scanCount || 0}`);
|
|
272
|
+
console.log(` Score: ${data.score?.score ?? '?'}/100 ${data.score?.grade ?? ''}`);
|
|
273
|
+
console.log(` Findings: ${data.score?.totalFindings ?? 0}`);
|
|
274
|
+
|
|
275
|
+
if (data.agentic) {
|
|
276
|
+
console.log(` Agentic: ${data.agentic.flagged}/${data.agentic.total} OWASP Agentic risks flagged`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Severity breakdown
|
|
280
|
+
const sevCounts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
281
|
+
for (const f of (data.findings || [])) {
|
|
282
|
+
sevCounts[f.severity] = (sevCounts[f.severity] || 0) + 1;
|
|
283
|
+
}
|
|
284
|
+
console.log(` Critical: ${sevCounts.critical}`);
|
|
285
|
+
console.log(` High: ${sevCounts.high}`);
|
|
286
|
+
console.log(` Medium: ${sevCounts.medium}`);
|
|
287
|
+
console.log(` Low: ${sevCounts.low}\n`);
|
|
288
|
+
} catch {
|
|
289
|
+
console.log('\n Failed to read watch data. File may be corrupted.\n');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// =============================================================================
|
|
294
|
+
// DEEP WATCH MODE (full orchestrator)
|
|
295
|
+
// =============================================================================
|
|
296
|
+
|
|
297
|
+
async function watchStateful(absolutePath, options = {}) {
|
|
298
|
+
const { StatefulWatcher } = await import('../agents/stateful-watcher.js');
|
|
299
|
+
const { ReconAgent } = await import('../agents/recon-agent.js');
|
|
300
|
+
|
|
301
|
+
const debounceMs = options.debounce || 2000;
|
|
302
|
+
const scoringEngine = new ScoringEngine();
|
|
303
|
+
|
|
304
|
+
console.log();
|
|
305
|
+
output.header('Praxis — Stateful Watch Mode (Kimi K2.6)');
|
|
306
|
+
console.log();
|
|
307
|
+
console.log(chalk.cyan(' Persistent security session — context builds over time'));
|
|
308
|
+
console.log(chalk.gray(` Debounce: ${debounceMs}ms`));
|
|
309
|
+
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
310
|
+
console.log();
|
|
311
|
+
|
|
312
|
+
const watcher = StatefulWatcher.create(absolutePath, {
|
|
313
|
+
provider: options.provider || 'kimi',
|
|
314
|
+
model: options.model || 'kimi-k2.6',
|
|
315
|
+
verbose: options.verbose,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (!watcher) {
|
|
319
|
+
output.error('Stateful watch requires MOONSHOT_API_KEY. Set it and retry.');
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Prime session with baseline
|
|
324
|
+
const reconAgent = new ReconAgent();
|
|
325
|
+
console.log(chalk.gray(' Building baseline...'));
|
|
326
|
+
let recon;
|
|
327
|
+
try {
|
|
328
|
+
const reconResult = await reconAgent.analyze({ rootPath: absolutePath });
|
|
329
|
+
recon = Array.isArray(reconResult) ? {} : reconResult;
|
|
330
|
+
} catch { recon = {}; }
|
|
331
|
+
const files = await reconAgent.discoverFiles(absolutePath);
|
|
332
|
+
await watcher.setBaseline(recon, files);
|
|
333
|
+
console.log(chalk.green(` Baseline set (${watcher.provider.name} / ${watcher.provider.model}). Watching...\n`));
|
|
334
|
+
|
|
335
|
+
let pendingFiles = new Set();
|
|
336
|
+
let debounceTimer = null;
|
|
337
|
+
let allFindings = [];
|
|
338
|
+
|
|
339
|
+
const dbDir = path.join(absolutePath, WATCH_DB_DIR);
|
|
340
|
+
const dbFile = path.join(dbDir, WATCH_DB_FILE);
|
|
341
|
+
|
|
342
|
+
const processChanges = async () => {
|
|
343
|
+
const changedFiles = [...pendingFiles];
|
|
344
|
+
pendingFiles.clear();
|
|
345
|
+
if (changedFiles.length === 0) return;
|
|
346
|
+
|
|
347
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
348
|
+
console.log(chalk.gray(` [${timestamp}] ${changedFiles.length} file(s) changed — stateful scan...`));
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const newFindings = await watcher.analyzeChanges(changedFiles);
|
|
352
|
+
|
|
353
|
+
if (newFindings.length === 0) {
|
|
354
|
+
console.log(chalk.green(` [${timestamp}] ✔ Clean\n`));
|
|
355
|
+
} else {
|
|
356
|
+
allFindings = allFindings.concat(newFindings);
|
|
357
|
+
const scoreResult = scoringEngine.compute(allFindings);
|
|
358
|
+
const scoreColor = scoreResult.score >= 75 ? chalk.cyan : scoreResult.score >= 50 ? chalk.yellow : chalk.red;
|
|
359
|
+
console.log(` [${timestamp}] ${chalk.white(`${newFindings.length} new finding(s)`)}: Score ${scoreColor(`${scoreResult.score}/100`)}`);
|
|
360
|
+
for (const f of newFindings.filter(f => f.severity === 'critical' || f.severity === 'high')) {
|
|
361
|
+
const relFile = path.relative(absolutePath, f.file || '');
|
|
362
|
+
const sev = f.severity === 'critical' ? chalk.red.bold('!!') : chalk.yellow(' !');
|
|
363
|
+
console.log(` ${sev} ${f.title} — ${relFile}:${f.line}`);
|
|
364
|
+
}
|
|
365
|
+
console.log('');
|
|
366
|
+
|
|
367
|
+
// Persist
|
|
368
|
+
try {
|
|
369
|
+
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
|
|
370
|
+
const stats = watcher.getStats();
|
|
371
|
+
fs.writeFileSync(dbFile, JSON.stringify({
|
|
372
|
+
mode: 'stateful',
|
|
373
|
+
lastScan: new Date().toISOString(),
|
|
374
|
+
scanCount: stats.scanCount,
|
|
375
|
+
provider: stats.provider,
|
|
376
|
+
model: stats.model,
|
|
377
|
+
findings: allFindings.map(f => ({
|
|
378
|
+
file: path.relative(absolutePath, f.file || ''),
|
|
379
|
+
line: f.line,
|
|
380
|
+
severity: f.severity,
|
|
381
|
+
rule: f.rule,
|
|
382
|
+
title: f.title,
|
|
383
|
+
})),
|
|
384
|
+
}, null, 2));
|
|
385
|
+
} catch { /* non-fatal */ }
|
|
386
|
+
}
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.log(chalk.red(` [${timestamp}] Scan error: ${err.message}\n`));
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const fsWatcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
394
|
+
if (!filename) return;
|
|
395
|
+
const relPath = filename.replace(/\\/g, '/');
|
|
396
|
+
for (const skipDir of SKIP_DIRS) {
|
|
397
|
+
if (relPath.includes(`${skipDir}/`)) return;
|
|
398
|
+
}
|
|
399
|
+
const ext = path.extname(filename).toLowerCase();
|
|
400
|
+
if (SKIP_EXTENSIONS.has(ext)) return;
|
|
401
|
+
if (SKIP_FILENAMES.has(path.basename(filename))) return;
|
|
402
|
+
if (filename.endsWith('.min.js') || filename.endsWith('.min.css')) return;
|
|
403
|
+
|
|
404
|
+
const fullPath = path.join(absolutePath, filename); // praxis-ignore API_PATH_IN_FILENAME — filename from fs.watch OS events, path.join with absolute base
|
|
405
|
+
if (!fs.existsSync(fullPath)) return;
|
|
406
|
+
|
|
407
|
+
pendingFiles.add(fullPath);
|
|
408
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
409
|
+
debounceTimer = setTimeout(processChanges, debounceMs);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
process.on('SIGINT', () => {
|
|
413
|
+
fsWatcher.close();
|
|
414
|
+
const stats = watcher.getStats();
|
|
415
|
+
console.log(`\n Stateful watch stopped. ${stats.scanCount} scan(s), ${allFindings.length} total finding(s).\n`);
|
|
416
|
+
process.exit(0);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
setInterval(() => {}, 1000 * 60 * 60);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
output.error(`Stateful watch failed: ${err.message}`);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function watchDeep(absolutePath, options = {}) {
|
|
427
|
+
const { buildOrchestratorAsync } = await import('../agents/index.js');
|
|
428
|
+
const { ReconAgent } = await import('../agents/recon-agent.js');
|
|
429
|
+
|
|
430
|
+
const debounceMs = options.debounce || 1500;
|
|
431
|
+
const threshold = options.threshold || null;
|
|
432
|
+
const slackWebhook = options.slack || process.env.PRAXIS_SLACK_WEBHOOK || null;
|
|
433
|
+
const prComments = options.prComment || false;
|
|
434
|
+
const scoringEngine = new ScoringEngine();
|
|
435
|
+
|
|
436
|
+
console.log();
|
|
437
|
+
output.header('Praxis — Deep Watch Mode');
|
|
438
|
+
console.log();
|
|
439
|
+
console.log(chalk.cyan(' Running full agent scans on file changes'));
|
|
440
|
+
console.log(chalk.gray(` Debounce: ${debounceMs}ms`));
|
|
441
|
+
if (threshold) console.log(chalk.gray(` Threshold: ${threshold}/100`));
|
|
442
|
+
if (slackWebhook) console.log(chalk.gray(' Slack: notifications enabled'));
|
|
443
|
+
if (prComments) console.log(chalk.gray(' PR: inline comments enabled (requires gh CLI)'));
|
|
444
|
+
console.log(chalk.gray(' Press Ctrl+C to stop'));
|
|
445
|
+
console.log();
|
|
446
|
+
|
|
447
|
+
// Initial recon
|
|
448
|
+
const reconAgent = new ReconAgent();
|
|
449
|
+
console.log(chalk.gray(' Running initial recon...'));
|
|
450
|
+
let recon;
|
|
451
|
+
try {
|
|
452
|
+
const reconResults = await reconAgent.analyze({ rootPath: absolutePath });
|
|
453
|
+
recon = Array.isArray(reconResults) ? {} : reconResults;
|
|
454
|
+
} catch { recon = {}; }
|
|
455
|
+
console.log(chalk.gray(' Recon complete. Watching...\n'));
|
|
456
|
+
|
|
457
|
+
let pendingFiles = new Set();
|
|
458
|
+
let debounceTimer = null;
|
|
459
|
+
let scanCount = 0;
|
|
460
|
+
|
|
461
|
+
const dbDir = path.join(absolutePath, WATCH_DB_DIR);
|
|
462
|
+
const dbFile = path.join(dbDir, WATCH_DB_FILE);
|
|
463
|
+
|
|
464
|
+
const processChanges = async () => {
|
|
465
|
+
const files = [...pendingFiles];
|
|
466
|
+
pendingFiles.clear();
|
|
467
|
+
if (files.length === 0) return;
|
|
468
|
+
|
|
469
|
+
scanCount++;
|
|
470
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
471
|
+
console.log(chalk.gray(` [${timestamp}] ${files.length} file(s) changed — deep scanning...`));
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const orchestrator = await buildOrchestratorAsync(absolutePath, { quiet: true });
|
|
475
|
+
const context = {
|
|
476
|
+
rootPath: absolutePath,
|
|
477
|
+
files,
|
|
478
|
+
changedFiles: files,
|
|
479
|
+
recon,
|
|
480
|
+
options: { incremental: true },
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const findings = await orchestrator.run(context);
|
|
484
|
+
const scoreResult = scoringEngine.compute(findings);
|
|
485
|
+
|
|
486
|
+
// Persist results
|
|
487
|
+
try {
|
|
488
|
+
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
|
|
489
|
+
fs.writeFileSync(dbFile, JSON.stringify({
|
|
490
|
+
lastScan: new Date().toISOString(),
|
|
491
|
+
scanCount,
|
|
492
|
+
score: {
|
|
493
|
+
score: scoreResult.score,
|
|
494
|
+
grade: scoreResult.grade?.letter,
|
|
495
|
+
totalFindings: scoreResult.totalFindings,
|
|
496
|
+
},
|
|
497
|
+
agentic: scoreResult.agenticSummary
|
|
498
|
+
? { flagged: scoreResult.agenticSummary.flagged, total: scoreResult.agenticSummary.total }
|
|
499
|
+
: null,
|
|
500
|
+
findings: findings.map(f => ({
|
|
501
|
+
file: path.relative(absolutePath, f.file || ''),
|
|
502
|
+
line: f.line,
|
|
503
|
+
severity: f.severity,
|
|
504
|
+
rule: f.rule,
|
|
505
|
+
title: f.title,
|
|
506
|
+
agenticRisk: f.agenticRisk || null,
|
|
507
|
+
})),
|
|
508
|
+
}, null, 2));
|
|
509
|
+
} catch { /* non-fatal */ }
|
|
510
|
+
|
|
511
|
+
// Output
|
|
512
|
+
const criticals = findings.filter(f => f.severity === 'critical').length;
|
|
513
|
+
const highs = findings.filter(f => f.severity === 'high').length;
|
|
514
|
+
|
|
515
|
+
if (findings.length === 0) {
|
|
516
|
+
console.log(chalk.green(` [${timestamp}] ✔ Clean — Score: ${scoreResult.score}/100 ${scoreResult.grade?.letter}\n`));
|
|
517
|
+
} else {
|
|
518
|
+
const scoreColor = scoreResult.score >= 75 ? chalk.cyan : scoreResult.score >= 50 ? chalk.yellow : chalk.red;
|
|
519
|
+
console.log(` [${timestamp}] ${chalk.white(`${findings.length} finding(s)`)}: ${criticals ? chalk.red.bold(`${criticals} critical`) : ''}${criticals && highs ? ', ' : ''}${highs ? chalk.yellow(`${highs} high`) : ''}. Score: ${scoreColor(`${scoreResult.score}/100 ${scoreResult.grade?.letter}`)}`);
|
|
520
|
+
|
|
521
|
+
for (const f of findings.filter(f => f.severity === 'critical' || f.severity === 'high')) {
|
|
522
|
+
const relFile = path.relative(absolutePath, f.file || '');
|
|
523
|
+
const sev = f.severity === 'critical' ? chalk.red.bold('!!') : chalk.yellow(' !');
|
|
524
|
+
const agentic = f.agenticRisk ? chalk.gray(` [${f.agenticRisk.id}]`) : '';
|
|
525
|
+
console.log(` ${sev} ${f.title} — ${relFile}:${f.line}${agentic}`);
|
|
526
|
+
}
|
|
527
|
+
console.log('');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (threshold && scoreResult.score < threshold) {
|
|
531
|
+
console.log(chalk.red.bold(` ⚠ Score ${scoreResult.score} below threshold ${threshold}\n`));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Slack Notification ──────────────────────────────────────────────
|
|
535
|
+
if (slackWebhook && findings.length > 0) {
|
|
536
|
+
await postSlackAlert(slackWebhook, findings, scoreResult, absolutePath).catch(() => {});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ── GitHub PR Inline Comments ────────────────────────────────────────
|
|
540
|
+
if (prComments && findings.length > 0) {
|
|
541
|
+
await postPRComments(findings, absolutePath).catch(() => {});
|
|
542
|
+
}
|
|
543
|
+
} catch (err) {
|
|
544
|
+
console.log(chalk.red(` [${timestamp}] Scan error: ${err.message}\n`));
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const watcher = fs.watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
550
|
+
if (!filename) return;
|
|
551
|
+
|
|
552
|
+
// Skip non-scannable
|
|
553
|
+
const relPath = filename.replace(/\\/g, '/');
|
|
554
|
+
for (const skipDir of SKIP_DIRS) {
|
|
555
|
+
if (relPath.includes(`${skipDir}/`)) return;
|
|
556
|
+
}
|
|
557
|
+
const ext = path.extname(filename).toLowerCase();
|
|
558
|
+
if (SKIP_EXTENSIONS.has(ext)) return;
|
|
559
|
+
if (SKIP_FILENAMES.has(path.basename(filename))) return;
|
|
560
|
+
if (filename.endsWith('.min.js') || filename.endsWith('.min.css')) return;
|
|
561
|
+
|
|
562
|
+
const fullPath = path.join(absolutePath, filename); // praxis-ignore API_PATH_IN_FILENAME — filename from fs.watch OS events, path.join with absolute base
|
|
563
|
+
if (!fs.existsSync(fullPath)) return;
|
|
564
|
+
|
|
565
|
+
pendingFiles.add(fullPath);
|
|
566
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
567
|
+
debounceTimer = setTimeout(processChanges, debounceMs);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
process.on('SIGINT', () => {
|
|
571
|
+
watcher.close();
|
|
572
|
+
console.log(`\n Watch stopped. ${scanCount} scan(s) completed.\n`);
|
|
573
|
+
process.exit(0);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
setInterval(() => {}, 1000 * 60 * 60);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
output.error(`Watch failed: ${err.message}`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// =============================================================================
|
|
584
|
+
// SLACK NOTIFICATIONS
|
|
585
|
+
// =============================================================================
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Post a security alert to a Slack webhook.
|
|
589
|
+
* Webhook URL can be set via --slack or PRAXIS_SLACK_WEBHOOK env var.
|
|
590
|
+
*/
|
|
591
|
+
async function postSlackAlert(webhookUrl, findings, scoreResult, rootPath) {
|
|
592
|
+
const repoName = path.basename(rootPath);
|
|
593
|
+
const criticals = findings.filter(f => f.severity === 'critical').length;
|
|
594
|
+
const highs = findings.filter(f => f.severity === 'high').length;
|
|
595
|
+
|
|
596
|
+
const color = criticals > 0 ? 'danger' : highs > 0 ? 'warning' : 'good';
|
|
597
|
+
const emoji = criticals > 0 ? ':rotating_light:' : highs > 0 ? ':warning:' : ':shield:';
|
|
598
|
+
|
|
599
|
+
const topFindings = findings
|
|
600
|
+
.filter(f => f.severity === 'critical' || f.severity === 'high')
|
|
601
|
+
.slice(0, 5)
|
|
602
|
+
.map(f => `• *${f.severity.toUpperCase()}* ${f.title} — \`${f.file ? path.basename(f.file) : '?'}${f.line ? `:${f.line}` : ''}\``)
|
|
603
|
+
.join('\n');
|
|
604
|
+
|
|
605
|
+
const payload = {
|
|
606
|
+
attachments: [{
|
|
607
|
+
color,
|
|
608
|
+
fallback: `Praxis: ${findings.length} security finding(s) in ${repoName}`,
|
|
609
|
+
title: `${emoji} Praxis — Security Alert`,
|
|
610
|
+
text: `*${repoName}* — Score: *${scoreResult.score ?? '?'}/100* — ${findings.length} finding(s) (${criticals} critical, ${highs} high)`,
|
|
611
|
+
fields: topFindings ? [{ title: 'Top Findings', value: topFindings, short: false }] : [],
|
|
612
|
+
footer: 'praxis watch --deep',
|
|
613
|
+
ts: Math.floor(Date.now() / 1000),
|
|
614
|
+
}],
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const res = await fetch(webhookUrl, {
|
|
618
|
+
method: 'POST',
|
|
619
|
+
headers: { 'Content-Type': 'application/json' },
|
|
620
|
+
body: JSON.stringify(payload),
|
|
621
|
+
signal: AbortSignal.timeout(10000),
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
if (!res.ok) {
|
|
625
|
+
console.log(chalk.yellow(` [Slack] Notification failed: HTTP ${res.status}`));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// =============================================================================
|
|
630
|
+
// GITHUB PR INLINE COMMENTS
|
|
631
|
+
// =============================================================================
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Post inline security comments to the currently open PR (if any).
|
|
635
|
+
* Requires `gh` CLI to be installed and authenticated.
|
|
636
|
+
*
|
|
637
|
+
* Posts a review comment for each critical/high finding in a changed file.
|
|
638
|
+
*/
|
|
639
|
+
async function postPRComments(findings, rootPath) {
|
|
640
|
+
// Check if gh is available
|
|
641
|
+
try {
|
|
642
|
+
execFileSync('gh', ['--version'], { stdio: 'pipe' });
|
|
643
|
+
} catch {
|
|
644
|
+
console.log(chalk.gray(' [PR] gh CLI not found — skipping PR comments'));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Get current PR number
|
|
649
|
+
let prNumber;
|
|
650
|
+
try {
|
|
651
|
+
const prJson = execFileSync('gh', ['pr', 'view', '--json', 'number'], {
|
|
652
|
+
cwd: rootPath, encoding: 'utf-8', stdio: 'pipe',
|
|
653
|
+
});
|
|
654
|
+
prNumber = JSON.parse(prJson).number;
|
|
655
|
+
} catch {
|
|
656
|
+
return; // No open PR on this branch
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Get current commit SHA
|
|
660
|
+
let sha;
|
|
661
|
+
try {
|
|
662
|
+
sha = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
663
|
+
cwd: rootPath, encoding: 'utf-8', stdio: 'pipe',
|
|
664
|
+
}).trim();
|
|
665
|
+
} catch {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const criticalOrHigh = findings.filter(f =>
|
|
670
|
+
(f.severity === 'critical' || f.severity === 'high') && f.file && f.line
|
|
671
|
+
).slice(0, 10); // Max 10 comments per scan
|
|
672
|
+
|
|
673
|
+
for (const f of criticalOrHigh) {
|
|
674
|
+
const relFile = path.relative(rootPath, f.file).replace(/\\/g, '/');
|
|
675
|
+
const body = [
|
|
676
|
+
`**Praxis — ${f.severity.toUpperCase()} finding**`,
|
|
677
|
+
'',
|
|
678
|
+
`**${f.title}**`,
|
|
679
|
+
f.description || '',
|
|
680
|
+
'',
|
|
681
|
+
f.remediation ? `**Fix:** ${f.remediation}` : '',
|
|
682
|
+
'',
|
|
683
|
+
`_[${f.rule}] — detected by praxis watch_`,
|
|
684
|
+
].filter(l => l !== undefined).join('\n');
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
execFileSync('gh', [
|
|
688
|
+
'api',
|
|
689
|
+
`repos/{owner}/{repo}/pulls/${prNumber}/comments`,
|
|
690
|
+
'--method', 'POST',
|
|
691
|
+
'--field', `body=${body}`,
|
|
692
|
+
'--field', `commit_id=${sha}`,
|
|
693
|
+
'--field', `path=${relFile}`,
|
|
694
|
+
'--field', `line=${f.line}`,
|
|
695
|
+
'--field', 'side=RIGHT',
|
|
696
|
+
], { cwd: rootPath, stdio: 'pipe' });
|
|
697
|
+
} catch { /* individual comment failure is non-fatal */ }
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (criticalOrHigh.length > 0) {
|
|
701
|
+
console.log(chalk.gray(` [PR #${prNumber}] Posted ${criticalOrHigh.length} inline comment(s)`));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// =============================================================================
|
|
706
|
+
// CONFIG WATCH — scanConfigFiles
|
|
707
|
+
// =============================================================================
|
|
708
|
+
|
|
709
|
+
async function scanConfigFiles(files, rootPath) {
|
|
710
|
+
// Dynamic import to avoid circular dependency
|
|
711
|
+
const { AgentConfigScanner } = await import('../agents/agent-config-scanner.js');
|
|
712
|
+
const { MCPSecurityAgent } = await import('../agents/mcp-security-agent.js');
|
|
713
|
+
|
|
714
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
715
|
+
const scanner = new AgentConfigScanner();
|
|
716
|
+
const mcpScanner = new MCPSecurityAgent();
|
|
717
|
+
|
|
718
|
+
for (const filePath of files) {
|
|
719
|
+
if (!fs.existsSync(filePath)) {
|
|
720
|
+
console.log(chalk.gray(` [${timestamp}] ${path.relative(rootPath, filePath)} — deleted`));
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const relPath = path.relative(rootPath, filePath).replace(/\\/g, '/');
|
|
725
|
+
console.log(chalk.cyan(` [${timestamp}] Changed: ${relPath}`));
|
|
726
|
+
|
|
727
|
+
// Git blame (best-effort)
|
|
728
|
+
try {
|
|
729
|
+
const { execFileSync } = await import('child_process');
|
|
730
|
+
const blame = execFileSync('git', ['log', '-1', '--format=%an (%ar)', '--', filePath], { cwd: rootPath, encoding: 'utf-8', timeout: 5000 }).trim();
|
|
731
|
+
if (blame) console.log(chalk.gray(` Last modified by: ${blame}`));
|
|
732
|
+
} catch { /* not a git repo or git not available */ }
|
|
733
|
+
|
|
734
|
+
// Run agent config scanner
|
|
735
|
+
const context = { rootPath, files: [] };
|
|
736
|
+
const [configFindings, mcpFindings] = await Promise.all([
|
|
737
|
+
scanner.analyze(context),
|
|
738
|
+
mcpScanner.analyze(context),
|
|
739
|
+
]);
|
|
740
|
+
|
|
741
|
+
const findings = [...configFindings, ...mcpFindings].filter(f =>
|
|
742
|
+
f.file && path.resolve(f.file) === path.resolve(filePath)
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
if (findings.length > 0) {
|
|
746
|
+
for (const f of findings) {
|
|
747
|
+
const sevColor = f.severity === 'critical' ? chalk.red.bold
|
|
748
|
+
: f.severity === 'high' ? chalk.yellow
|
|
749
|
+
: chalk.blue;
|
|
750
|
+
console.log(` ${sevColor(`[${f.severity.toUpperCase()}]`)} ${f.title || f.rule}`);
|
|
751
|
+
}
|
|
752
|
+
} else {
|
|
753
|
+
console.log(chalk.green(' ✔ Clean'));
|
|
754
|
+
}
|
|
755
|
+
console.log();
|
|
756
|
+
}
|
|
757
|
+
}
|