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,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init Command
|
|
3
|
+
* ============
|
|
4
|
+
*
|
|
5
|
+
* Initialize security configurations in the current project.
|
|
6
|
+
* Copies pre-configured security files from praxis.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* praxis init Copy all security configs
|
|
10
|
+
* praxis init --gitignore Only copy .gitignore
|
|
11
|
+
* praxis init --headers Only copy security headers
|
|
12
|
+
* praxis init -f Force overwrite existing files
|
|
13
|
+
*
|
|
14
|
+
* FILES COPIED:
|
|
15
|
+
* - .gitignore (merged with existing if present)
|
|
16
|
+
* - nextjs-security-headers.js (for Next.js projects)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import chalk from 'chalk';
|
|
23
|
+
import * as output from '../utils/output.js';
|
|
24
|
+
|
|
25
|
+
// Get the directory of this module (for finding config files)
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url); // praxis-ignore — module's own path via import.meta.url, not user input
|
|
27
|
+
const __dirname = path.dirname(__filename);
|
|
28
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// MAIN INIT FUNCTION
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
export async function initCommand(options = {}) {
|
|
35
|
+
const targetDir = process.cwd();
|
|
36
|
+
|
|
37
|
+
console.log();
|
|
38
|
+
output.header('Initializing Security Configs');
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(chalk.gray(`Target directory: ${targetDir}`));
|
|
41
|
+
console.log();
|
|
42
|
+
|
|
43
|
+
const results = {
|
|
44
|
+
copied: [],
|
|
45
|
+
skipped: [],
|
|
46
|
+
merged: [],
|
|
47
|
+
errors: []
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Determine which files to copy.
|
|
51
|
+
// If a specific flag is set, only run that category.
|
|
52
|
+
// With no flags, run everything.
|
|
53
|
+
// Handle --openclaw flag separately
|
|
54
|
+
if (options.openclaw) {
|
|
55
|
+
return handleOpenClawInit(targetDir, options.force, results);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle --hermes --from <url>
|
|
59
|
+
if (options.hermes) {
|
|
60
|
+
return handleHermesInit(targetDir, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const hasSpecificFlag = options.gitignore || options.headers || options.agents;
|
|
64
|
+
const copyGitignore = hasSpecificFlag ? !!options.gitignore : true;
|
|
65
|
+
const copyHeaders = hasSpecificFlag ? !!options.headers : true;
|
|
66
|
+
const copyAgents = hasSpecificFlag ? !!options.agents : true;
|
|
67
|
+
|
|
68
|
+
// Copy .gitignore
|
|
69
|
+
if (copyGitignore) {
|
|
70
|
+
await handleGitignore(targetDir, options.force, results);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Copy security headers
|
|
74
|
+
if (copyHeaders) {
|
|
75
|
+
await handleSecurityHeaders(targetDir, options.force, results);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Append security rules to AI agent instruction files
|
|
79
|
+
if (copyAgents) {
|
|
80
|
+
await handleAgentFiles(targetDir, options.force, results);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Print summary
|
|
84
|
+
printSummary(results);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// =============================================================================
|
|
88
|
+
// GITIGNORE HANDLING
|
|
89
|
+
// =============================================================================
|
|
90
|
+
|
|
91
|
+
async function handleGitignore(targetDir, force, results) {
|
|
92
|
+
// Note: We use 'gitignore-template' because npm excludes dotfiles from packages
|
|
93
|
+
const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'gitignore-template');
|
|
94
|
+
const targetPath = path.join(targetDir, '.gitignore');
|
|
95
|
+
|
|
96
|
+
// Check if source exists
|
|
97
|
+
if (!fs.existsSync(sourcePath)) {
|
|
98
|
+
results.errors.push({
|
|
99
|
+
file: '.gitignore',
|
|
100
|
+
error: 'Source file not found in praxis package'
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
|
|
106
|
+
|
|
107
|
+
// Check if target exists
|
|
108
|
+
if (fs.existsSync(targetPath)) {
|
|
109
|
+
if (force) {
|
|
110
|
+
// Overwrite
|
|
111
|
+
fs.writeFileSync(targetPath, sourceContent);
|
|
112
|
+
results.copied.push('.gitignore (overwritten)');
|
|
113
|
+
} else {
|
|
114
|
+
// Merge: append praxis patterns to existing
|
|
115
|
+
const existingContent = fs.readFileSync(targetPath, 'utf-8');
|
|
116
|
+
|
|
117
|
+
// Check if already has praxis content
|
|
118
|
+
if (existingContent.includes('# PRAXIS')) {
|
|
119
|
+
results.skipped.push('.gitignore (already contains praxis patterns)');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Append praxis section
|
|
124
|
+
const mergedContent = existingContent.trim() + '\n\n' +
|
|
125
|
+
'# =============================================================================\n' +
|
|
126
|
+
'# PRAXIS ADDITIONS\n' +
|
|
127
|
+
'# Added by: npx praxis-sec init\n' +
|
|
128
|
+
'# =============================================================================\n\n' +
|
|
129
|
+
extractSecurityPatterns(sourceContent);
|
|
130
|
+
|
|
131
|
+
fs.writeFileSync(targetPath, mergedContent);
|
|
132
|
+
results.merged.push('.gitignore');
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// Create new
|
|
136
|
+
fs.writeFileSync(targetPath, sourceContent);
|
|
137
|
+
results.copied.push('.gitignore');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract the most important security patterns from our .gitignore
|
|
143
|
+
*/
|
|
144
|
+
function extractSecurityPatterns(fullGitignore) {
|
|
145
|
+
// Extract key sections
|
|
146
|
+
const patterns = `
|
|
147
|
+
# Environment files
|
|
148
|
+
.env
|
|
149
|
+
.env.local
|
|
150
|
+
.env*.local
|
|
151
|
+
*.env
|
|
152
|
+
|
|
153
|
+
# Private keys & certificates
|
|
154
|
+
*.pem
|
|
155
|
+
*.key
|
|
156
|
+
*.p12
|
|
157
|
+
*.pfx
|
|
158
|
+
|
|
159
|
+
# Credentials
|
|
160
|
+
*credentials*
|
|
161
|
+
*.secrets.json
|
|
162
|
+
secrets.yml
|
|
163
|
+
secrets.yaml
|
|
164
|
+
|
|
165
|
+
# Service accounts
|
|
166
|
+
**/service-account*.json
|
|
167
|
+
*-firebase-adminsdk-*.json
|
|
168
|
+
|
|
169
|
+
# AWS
|
|
170
|
+
.aws/credentials
|
|
171
|
+
|
|
172
|
+
# Database files
|
|
173
|
+
*.sqlite
|
|
174
|
+
*.sqlite3
|
|
175
|
+
*.db
|
|
176
|
+
|
|
177
|
+
# Logs (may contain sensitive data)
|
|
178
|
+
*.log
|
|
179
|
+
logs/
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
return patterns.trim();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// =============================================================================
|
|
186
|
+
// SECURITY HEADERS HANDLING
|
|
187
|
+
// =============================================================================
|
|
188
|
+
|
|
189
|
+
async function handleSecurityHeaders(targetDir, force, results) {
|
|
190
|
+
const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'nextjs-security-headers.js');
|
|
191
|
+
const targetPath = path.join(targetDir, 'security-headers.config.js');
|
|
192
|
+
|
|
193
|
+
// Check if source exists
|
|
194
|
+
if (!fs.existsSync(sourcePath)) {
|
|
195
|
+
results.errors.push({
|
|
196
|
+
file: 'security-headers.config.js',
|
|
197
|
+
error: 'Source file not found in praxis package'
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Detect if this is a Next.js project
|
|
203
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
204
|
+
let isNextProject = false;
|
|
205
|
+
|
|
206
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
207
|
+
try {
|
|
208
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
209
|
+
isNextProject = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
210
|
+
} catch {
|
|
211
|
+
// Ignore parse errors
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check if target exists
|
|
216
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
217
|
+
results.skipped.push('security-headers.config.js (already exists, use -f to overwrite)');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Copy the file
|
|
222
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
223
|
+
fs.writeFileSync(targetPath, content);
|
|
224
|
+
results.copied.push('security-headers.config.js');
|
|
225
|
+
|
|
226
|
+
// Show integration instructions
|
|
227
|
+
if (isNextProject) {
|
|
228
|
+
console.log(chalk.cyan('\nNext.js detected! Add to your next.config.js:\n'));
|
|
229
|
+
console.log(chalk.gray(' const { securityHeadersConfig } = require(\'./security-headers.config.js\');'));
|
|
230
|
+
console.log(chalk.gray(' module.exports = { ...securityHeadersConfig, /* your config */ };'));
|
|
231
|
+
console.log();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// =============================================================================
|
|
236
|
+
// AGENT FILE HANDLING
|
|
237
|
+
// =============================================================================
|
|
238
|
+
|
|
239
|
+
// Marker used for idempotency — if this string is present we already added our section.
|
|
240
|
+
const AGENT_MARKER = '<!-- praxis -->';
|
|
241
|
+
|
|
242
|
+
// The security rules block appended to existing agent instruction files.
|
|
243
|
+
const AGENT_SECTION = `
|
|
244
|
+
## Security (praxis)
|
|
245
|
+
${AGENT_MARKER}
|
|
246
|
+
- Always run \`npx praxis-sec scan .\` before committing code
|
|
247
|
+
- Never hardcode API keys, tokens, or credentials in source files — use environment variables
|
|
248
|
+
- If hardcoded secrets are found or generated, run \`npx praxis-sec remediate\` to auto-fix them
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
// Standalone content for a new Cursor rules file.
|
|
252
|
+
const CURSOR_RULE_CONTENT = `---
|
|
253
|
+
description: Prevent API key leaks and credential exposure in generated code
|
|
254
|
+
alwaysApply: true
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
- Always run \`npx praxis-sec scan .\` before committing code
|
|
258
|
+
- Never hardcode API keys, tokens, or credentials in source files — use environment variables
|
|
259
|
+
- If hardcoded secrets are found or generated, run \`npx praxis-sec remediate\` to auto-fix them
|
|
260
|
+
`;
|
|
261
|
+
|
|
262
|
+
async function handleAgentFiles(targetDir, force, results) {
|
|
263
|
+
// Files where we append a section if they already exist, or create if they don't.
|
|
264
|
+
const appendTargets = [
|
|
265
|
+
{ file: 'CLAUDE.md', label: 'CLAUDE.md' },
|
|
266
|
+
{ file: '.windsurfrules', label: '.windsurfrules' },
|
|
267
|
+
{ file: path.join('.github', 'copilot-instructions.md'), label: '.github/copilot-instructions.md' },
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
for (const { file, label } of appendTargets) {
|
|
271
|
+
const targetPath = path.join(targetDir, file);
|
|
272
|
+
|
|
273
|
+
if (fs.existsSync(targetPath)) {
|
|
274
|
+
const existing = fs.readFileSync(targetPath, 'utf-8');
|
|
275
|
+
if (existing.includes(AGENT_MARKER)) {
|
|
276
|
+
results.skipped.push(`${label} (already contains praxis rules)`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (force || true) { // always append unless already present
|
|
280
|
+
fs.writeFileSync(targetPath, existing.trimEnd() + '\n' + AGENT_SECTION);
|
|
281
|
+
results.merged.push(label);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
// Ensure parent directory exists (e.g. .github/)
|
|
285
|
+
const parentDir = path.dirname(targetPath);
|
|
286
|
+
if (!fs.existsSync(parentDir)) {
|
|
287
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
fs.writeFileSync(targetPath, AGENT_SECTION.trim() + '\n');
|
|
290
|
+
results.copied.push(label);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Cursor rules — dedicated file, no merging needed.
|
|
295
|
+
const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
|
|
296
|
+
const cursorRulesFile = path.join(cursorRulesDir, 'praxis.mdc');
|
|
297
|
+
|
|
298
|
+
if (fs.existsSync(cursorRulesFile) && !force) {
|
|
299
|
+
results.skipped.push('.cursor/rules/praxis.mdc (already exists, use -f to overwrite)');
|
|
300
|
+
} else {
|
|
301
|
+
if (!fs.existsSync(cursorRulesDir)) {
|
|
302
|
+
fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
303
|
+
}
|
|
304
|
+
fs.writeFileSync(cursorRulesFile, CURSOR_RULE_CONTENT.trim() + '\n');
|
|
305
|
+
results.copied.push('.cursor/rules/praxis.mdc');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// =============================================================================
|
|
310
|
+
// HERMES AGENT INIT
|
|
311
|
+
// =============================================================================
|
|
312
|
+
|
|
313
|
+
async function handleHermesInit(targetDir, options) {
|
|
314
|
+
const fromUrl = options.from;
|
|
315
|
+
|
|
316
|
+
if (!fromUrl) {
|
|
317
|
+
console.error(chalk.red('\nError: --hermes requires --from <setup-url>'));
|
|
318
|
+
console.error(chalk.gray(' Generate a setup URL at: '));
|
|
319
|
+
console.error(chalk.gray(' Then run: npx praxis-sec init --hermes --from <url>\n'));
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Validate the URL is well-formed and from a trusted origin.
|
|
324
|
+
// Allow HTTPS (any host — the security is HTTPS + explicit user opt-in via
|
|
325
|
+
// `praxis init --from <url>`) and localhost for local development.
|
|
326
|
+
let parsed;
|
|
327
|
+
try {
|
|
328
|
+
parsed = new URL(fromUrl);
|
|
329
|
+
} catch {
|
|
330
|
+
console.error(chalk.red('\nError: Invalid URL: ' + fromUrl + '\n'));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const isLocal = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1';
|
|
335
|
+
const isHttps = parsed.protocol === 'https:';
|
|
336
|
+
if (!isLocal && !isHttps) {
|
|
337
|
+
console.error(chalk.red('\nError: Setup URL must use HTTPS (got: ' + parsed.protocol + '//)'));
|
|
338
|
+
console.error(chalk.gray(' Or use http://localhost for local development.\n'));
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log();
|
|
343
|
+
output.header('Hermes Agent Security Setup');
|
|
344
|
+
console.log();
|
|
345
|
+
console.log(chalk.gray('Fetching config from:'), chalk.cyan(fromUrl));
|
|
346
|
+
console.log();
|
|
347
|
+
|
|
348
|
+
// Fetch the config bundle
|
|
349
|
+
let data;
|
|
350
|
+
try {
|
|
351
|
+
const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }));
|
|
352
|
+
const res = await fetch(fromUrl, { headers: { 'Accept': 'application/json' } });
|
|
353
|
+
if (!res.ok) {
|
|
354
|
+
const body = await res.json().catch(() => ({}));
|
|
355
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
356
|
+
}
|
|
357
|
+
data = await res.json();
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.error(chalk.red('\nFailed to fetch setup config: ' + err.message + '\n'));
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!data.files || !Array.isArray(data.files) || data.files.length === 0) {
|
|
364
|
+
console.error(chalk.red('\nInvalid config bundle — no files returned.\n'));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Write each file
|
|
369
|
+
const written = [];
|
|
370
|
+
const skipped = [];
|
|
371
|
+
|
|
372
|
+
for (const { path: filePath, content } of data.files) {
|
|
373
|
+
// Sanitize path — no traversal
|
|
374
|
+
const normalized = path.normalize(filePath).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
375
|
+
const absPath = path.join(targetDir, normalized);
|
|
376
|
+
|
|
377
|
+
if (fs.existsSync(absPath) && !options.force) {
|
|
378
|
+
skipped.push(normalized);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const dir = path.dirname(absPath);
|
|
383
|
+
if (!fs.existsSync(dir)) {
|
|
384
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
385
|
+
}
|
|
386
|
+
fs.writeFileSync(absPath, content, 'utf-8');
|
|
387
|
+
written.push(normalized);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Print results
|
|
391
|
+
console.log(chalk.green.bold('Files written:'));
|
|
392
|
+
for (const f of written) {
|
|
393
|
+
console.log(chalk.green(` ✔ ${f}`));
|
|
394
|
+
}
|
|
395
|
+
if (skipped.length > 0) {
|
|
396
|
+
console.log();
|
|
397
|
+
console.log(chalk.yellow.bold('Skipped (already exist — use -f to overwrite):'));
|
|
398
|
+
for (const f of skipped) {
|
|
399
|
+
console.log(chalk.yellow(` → ${f}`));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(chalk.cyan.bold('Next steps:'));
|
|
405
|
+
console.log(chalk.white(' 1.') + ' Populate your baseline: ' + chalk.cyan('npx praxis-sec audit .'));
|
|
406
|
+
console.log(chalk.white(' 2.') + ' Auto-fix findings: ' + chalk.cyan('npx praxis-sec audit . --agentic 3 --agentic-target 80'));
|
|
407
|
+
console.log(chalk.white(' 3.') + ' Commit everything and push — CI runs on every PR.');
|
|
408
|
+
console.log();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// =============================================================================
|
|
412
|
+
// OPENCLAW HARDENED CONFIG
|
|
413
|
+
// =============================================================================
|
|
414
|
+
|
|
415
|
+
const HARDENED_OPENCLAW = `{
|
|
416
|
+
"// SECURITY": "Generated by praxis init --openclaw — hardened defaults",
|
|
417
|
+
|
|
418
|
+
"// host": "Bind to localhost only — never 0.0.0.0 (CVE-2026-25253 ClawJacked)",
|
|
419
|
+
"host": "127.0.0.1",
|
|
420
|
+
"port": 3100,
|
|
421
|
+
|
|
422
|
+
"// auth": "Always require authentication — prevents unauthorized agent takeover",
|
|
423
|
+
"auth": {
|
|
424
|
+
"type": "apiKey",
|
|
425
|
+
"key": "\${OPENCLAW_API_KEY}"
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
"// url": "Use wss:// for all non-localhost connections (encrypted WebSocket)",
|
|
429
|
+
"url": "wss://localhost:3100",
|
|
430
|
+
|
|
431
|
+
"// safeBins": "Allowlist of binaries the agent can execute — block everything else",
|
|
432
|
+
"safeBins": ["node", "git", "npx", "npm"],
|
|
433
|
+
|
|
434
|
+
"// skills": "Only add verified skills from trusted sources — ClawHavoc had 1,184 malicious skills",
|
|
435
|
+
"skills": [],
|
|
436
|
+
|
|
437
|
+
"// logging": "Enable audit logging for security monitoring",
|
|
438
|
+
"logging": {
|
|
439
|
+
"level": "info",
|
|
440
|
+
"auditLog": true
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
`;
|
|
444
|
+
|
|
445
|
+
async function handleOpenClawInit(targetDir, force, results) {
|
|
446
|
+
const targetPath = path.join(targetDir, 'openclaw.json');
|
|
447
|
+
|
|
448
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
449
|
+
results.skipped.push('openclaw.json (already exists, use -f to overwrite)');
|
|
450
|
+
} else {
|
|
451
|
+
fs.writeFileSync(targetPath, HARDENED_OPENCLAW.trim() + '\n');
|
|
452
|
+
results.copied.push('openclaw.json (hardened template)');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
printSummary(results);
|
|
456
|
+
|
|
457
|
+
console.log(chalk.cyan('Important:'));
|
|
458
|
+
console.log(chalk.white(' 1.') + ' Set the OPENCLAW_API_KEY environment variable');
|
|
459
|
+
console.log(chalk.white(' 2.') + ' Only add verified skills from trusted sources');
|
|
460
|
+
console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx praxis-sec openclaw .') + ' to verify security');
|
|
461
|
+
console.log();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// =============================================================================
|
|
465
|
+
// SUMMARY
|
|
466
|
+
// =============================================================================
|
|
467
|
+
|
|
468
|
+
function printSummary(results) {
|
|
469
|
+
console.log();
|
|
470
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
471
|
+
console.log(chalk.cyan.bold(' Summary'));
|
|
472
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
473
|
+
console.log();
|
|
474
|
+
|
|
475
|
+
if (results.copied.length > 0) {
|
|
476
|
+
console.log(chalk.green.bold('Created:'));
|
|
477
|
+
for (const file of results.copied) {
|
|
478
|
+
console.log(chalk.green(` \u2714 ${file}`));
|
|
479
|
+
}
|
|
480
|
+
console.log();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (results.merged.length > 0) {
|
|
484
|
+
console.log(chalk.blue.bold('Merged:'));
|
|
485
|
+
for (const file of results.merged) {
|
|
486
|
+
console.log(chalk.blue(` \u2194 ${file} (appended praxis patterns)`));
|
|
487
|
+
}
|
|
488
|
+
console.log();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (results.skipped.length > 0) {
|
|
492
|
+
console.log(chalk.yellow.bold('Skipped:'));
|
|
493
|
+
for (const file of results.skipped) {
|
|
494
|
+
console.log(chalk.yellow(` \u2192 ${file}`));
|
|
495
|
+
}
|
|
496
|
+
console.log();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (results.errors.length > 0) {
|
|
500
|
+
console.log(chalk.red.bold('Errors:'));
|
|
501
|
+
for (const { file, error } of results.errors) {
|
|
502
|
+
console.log(chalk.red(` \u2718 ${file}: ${error}`));
|
|
503
|
+
}
|
|
504
|
+
console.log();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Next steps
|
|
508
|
+
console.log(chalk.cyan('Next steps:'));
|
|
509
|
+
console.log(chalk.white(' 1.') + ' Review the copied files and customize for your project');
|
|
510
|
+
console.log(chalk.white(' 2.') + ' Run ' + chalk.cyan('npx praxis-sec scan .') + ' to check for secrets');
|
|
511
|
+
console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx praxis-sec checklist') + ' before launching');
|
|
512
|
+
console.log();
|
|
513
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
514
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legal Command
|
|
3
|
+
* =============
|
|
4
|
+
*
|
|
5
|
+
* Scans dependency manifests for packages that carry legal risk:
|
|
6
|
+
* active DMCA takedowns, leaked-source derivatives, IP disputes,
|
|
7
|
+
* and license violations.
|
|
8
|
+
*
|
|
9
|
+
* USAGE:
|
|
10
|
+
* praxis legal [path] Scan for legally risky dependencies
|
|
11
|
+
* praxis legal . --json JSON output
|
|
12
|
+
*
|
|
13
|
+
* EXIT CODES:
|
|
14
|
+
* 0 Clean — no legally risky packages found
|
|
15
|
+
* 1 Findings — one or more legally risky packages detected
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import chalk from 'chalk';
|
|
21
|
+
import ora from 'ora';
|
|
22
|
+
import { LegalRiskAgent } from '../agents/legal-risk-agent.js';
|
|
23
|
+
import * as output from '../utils/output.js';
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// RISK LABELS & COLORS
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
const RISK_COLORS = {
|
|
30
|
+
dmca: chalk.red.bold,
|
|
31
|
+
'ip-dispute': chalk.red,
|
|
32
|
+
'leaked-source': chalk.yellow.bold,
|
|
33
|
+
'license-violation': chalk.yellow,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const RISK_LABELS = {
|
|
37
|
+
dmca: 'DMCA Takedown',
|
|
38
|
+
'ip-dispute': 'IP Dispute',
|
|
39
|
+
'leaked-source': 'Leaked Source',
|
|
40
|
+
'license-violation': 'License Violation',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const SEVERITY_COLORS = {
|
|
44
|
+
critical: chalk.bgRed.white.bold,
|
|
45
|
+
high: chalk.red.bold,
|
|
46
|
+
medium: chalk.yellow,
|
|
47
|
+
low: chalk.blue,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// MAIN COMMAND
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
export async function legalCommand(targetPath = '.', options = {}) {
|
|
55
|
+
const absolutePath = path.resolve(targetPath);
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(absolutePath)) {
|
|
58
|
+
output.error(`Path does not exist: ${absolutePath}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!options.json) {
|
|
63
|
+
console.log();
|
|
64
|
+
output.header('Legal Risk Audit');
|
|
65
|
+
console.log(chalk.gray(' Scanning for DMCA notices, leaked-source derivatives, and IP disputes'));
|
|
66
|
+
console.log();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Run the agent ──────────────────────────────────────────────────────────
|
|
70
|
+
const spinner = options.json
|
|
71
|
+
? null
|
|
72
|
+
: ora({ text: 'Scanning dependency manifests…', color: 'cyan' }).start();
|
|
73
|
+
|
|
74
|
+
const agent = new LegalRiskAgent();
|
|
75
|
+
let findings = [];
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
findings = await agent.analyze({ rootPath: absolutePath, files: [] });
|
|
79
|
+
if (spinner) spinner.stop();
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (spinner) spinner.stop();
|
|
82
|
+
output.error(`Legal scan failed: ${err.message}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── JSON output ────────────────────────────────────────────────────────────
|
|
87
|
+
if (options.json) {
|
|
88
|
+
console.log(JSON.stringify({ findings, total: findings.length }, null, 2));
|
|
89
|
+
process.exit(findings.length > 0 ? 1 : 0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Human-readable output ──────────────────────────────────────────────────
|
|
93
|
+
if (findings.length === 0) {
|
|
94
|
+
output.success('No legally risky packages found.');
|
|
95
|
+
console.log();
|
|
96
|
+
console.log(chalk.gray(' Scanned: package.json, requirements.txt, Cargo.toml, go.mod'));
|
|
97
|
+
console.log();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Group by severity
|
|
102
|
+
const bySeverity = { critical: [], high: [], medium: [], low: [] };
|
|
103
|
+
for (const f of findings) {
|
|
104
|
+
(bySeverity[f.severity] || bySeverity.medium).push(f);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const total = findings.length;
|
|
108
|
+
const critCount = bySeverity.critical.length;
|
|
109
|
+
const highCount = bySeverity.high.length;
|
|
110
|
+
|
|
111
|
+
// Summary line
|
|
112
|
+
console.log(
|
|
113
|
+
chalk.red.bold(` ${total} legal risk finding${total === 1 ? '' : 's'}`),
|
|
114
|
+
chalk.gray('—'),
|
|
115
|
+
critCount > 0 ? chalk.red.bold(`${critCount} critical`) + chalk.gray(', ') : '',
|
|
116
|
+
highCount > 0 ? chalk.red(`${highCount} high`) : '',
|
|
117
|
+
);
|
|
118
|
+
console.log();
|
|
119
|
+
|
|
120
|
+
// Print findings
|
|
121
|
+
for (const severity of ['critical', 'high', 'medium', 'low']) {
|
|
122
|
+
const group = bySeverity[severity];
|
|
123
|
+
if (group.length === 0) continue;
|
|
124
|
+
|
|
125
|
+
for (const f of group) {
|
|
126
|
+
const sevBadge = SEVERITY_COLORS[severity]
|
|
127
|
+
? SEVERITY_COLORS[severity](` ${severity.toUpperCase()} `)
|
|
128
|
+
: chalk.gray(` ${severity.toUpperCase()} `);
|
|
129
|
+
|
|
130
|
+
// Extract risk type from rule: LEGAL_RISK_DMCA → dmca
|
|
131
|
+
const riskKey = f.rule
|
|
132
|
+
.replace('LEGAL_RISK_', '')
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.replace(/_/g, '-');
|
|
135
|
+
const riskColor = RISK_COLORS[riskKey] || chalk.white;
|
|
136
|
+
const riskLabel = RISK_LABELS[riskKey] || riskKey;
|
|
137
|
+
|
|
138
|
+
console.log(` ${sevBadge} ${chalk.white.bold(f.title)}`);
|
|
139
|
+
console.log(` ${riskColor(`[${riskLabel}]`)} ${chalk.gray(path.relative(absolutePath, f.file) || f.file)}`);
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(` ${chalk.gray(f.description)}`);
|
|
142
|
+
console.log();
|
|
143
|
+
if (f.fix) {
|
|
144
|
+
console.log(` ${chalk.cyan('Fix:')} ${chalk.gray(f.fix)}`);
|
|
145
|
+
}
|
|
146
|
+
console.log();
|
|
147
|
+
console.log(chalk.gray(' ' + '─'.repeat(56)));
|
|
148
|
+
console.log();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Footer
|
|
153
|
+
console.log(chalk.yellow.bold(' ⚠ Shipping legally risky packages can expose your project to IP liability.'));
|
|
154
|
+
console.log(chalk.gray(' Review each finding and remove the affected dependency before releasing.'));
|
|
155
|
+
console.log();
|
|
156
|
+
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|