praxis-sec 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  <p align="center">
8
8
  <a href="https://github.com/Ganron007/Praxis/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Ganron007/Praxis/ci.yml?label=CI" alt="CI"></a>
9
- <a href="https://github.com/marketplace/actions/praxis-sec-scan"><img src="https://img.shields.io/badge/Marketplace-Praxis%20Security%20Scan-blue" alt="GitHub Marketplace"></a>
9
+ <a href="https://github.com/marketplace/actions/praxis-security-scan"><img src="https://img.shields.io/badge/Marketplace-Praxis%20Security%20Scan-blue" alt="GitHub Marketplace"></a>
10
10
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT"></a>
11
11
  <img src="https://img.shields.io/badge/Node.js-%E2%89%A518.0.0-blue.svg" alt="Node.js: >=18.0.0">
12
12
  <img src="https://img.shields.io/npm/v/praxis-sec?label=Version" alt="npm version">
@@ -40,14 +40,29 @@
40
40
  | **CI-native** | `scan ci` gates, SARIF for Code Scanning with real `security-severity` ranking, net-new PR gating (fails only on *introduced* findings), GitHub Action inline PR annotations |
41
41
  | **Reproducible** | Every scan reports a provenance fingerprint (tool, runtime, probe/threatpack/data versions), and CI enforces determinism between two runs |
42
42
 
43
+ ## Install
44
+
45
+ ```bash
46
+ npm install -g praxis-sec # then just run: praxis
47
+ npx praxis-sec scan . # or no install at all
48
+ ```
49
+
50
+ Requires Node.js 18+. Nothing else — no account, no API key. LLM-assisted remediation is
51
+ opt-in via `--deep`.
52
+
53
+ > **The package is `praxis-sec`, but the command is `praxis`.** The unscoped `praxis` name on
54
+ > npm belongs to an unrelated project, so the distribution carries a suffix. Installing it puts
55
+ > a `praxis` executable on your PATH, so every example below reads `praxis …`.
56
+
43
57
  ## Quick start
44
58
 
45
59
  ```bash
46
- npm install && npm link
60
+ npm install -g praxis-sec
47
61
 
48
62
  praxis scan . # full 28-agent audit + AST taint evaluation
63
+ praxis scan git <url> # direct remote Git repo audit (clones to temp dir & audits)
49
64
  praxis fix . # interactive LLM-guided fixes
50
- praxis redteam . # dynamic AI red team & DAST prober
65
+ praxis scan redteam . # adversarial agent pack against a local codebase
51
66
  praxis agents audit . # audit the AI/agent surface
52
67
  praxis agents mcp --test-live # live MCP JSON-RPC probe
53
68
  praxis web # local web UI for scans and findings
@@ -68,19 +83,25 @@ praxis vibe . # emoji-graded A–F score
68
83
  ## Command groups
69
84
 
70
85
  ```
71
- praxis scan secrets · full · changed · env · redteam · standard · ci
86
+ praxis scan full · git · secrets · changed · env · redteam · standard · ci
72
87
  praxis fix interactive · quick · from-report · rotate · undo · env-template
73
88
  praxis agents audit · skill · mcp · bom · serve (MCP server)
74
89
  praxis intel update · deps · advisories
75
90
  praxis report team · legal · checklist · sbom · benchmark
76
- praxis project init · doctor · hooks · guard · watch · baseline · plugins · policy
77
- praxis rules list · export · import (portable rule bundles)
78
- praxis web local web UI (read-only, loopback by default)
91
+ praxis project init · doctor · hooks · guard · watch · baseline · memory · playbook · plugins · policy
92
+ praxis rules list · export · import (portable rule bundles)
93
+ praxis web local web UI (read-only, loopback by default)
79
94
 
95
+ praxis redteam DAST red team against a live LLM endpoint
96
+ praxis hooks Claude Code tool-call security gate
80
97
  praxis vibe emoji-graded A–F score
81
98
  praxis score numeric score
82
99
  ```
83
100
 
101
+ > `praxis redteam` takes a **live endpoint** (e.g. `praxis redteam https://api.example.com`).
102
+ > To run the adversarial agent pack against a **local codebase**, use
103
+ > `praxis scan redteam <path>`.
104
+
84
105
  ## 28 agents at a glance
85
106
 
86
107
  | Cluster | Agents | Covers |
@@ -110,7 +131,7 @@ Template: [`.env.example`](.env.example) · Verify with `praxis project doctor`.
110
131
  ## CI
111
132
 
112
133
  ```yaml
113
- - uses: Ganron007/Praxis@master
134
+ - uses: Ganron007/Praxis@v1
114
135
  with:
115
136
  threshold: '80'
116
137
  net-new: 'true' # fail only on findings introduced by the PR
@@ -424,9 +424,13 @@ export class AiInfraInventoryAgent extends BaseAgent {
424
424
  // Eval-harness / sandbox misconfigurations (P-IMP-048)
425
425
  for (const check of EVAL_HARNESS_RISK) {
426
426
  if (check.regex.test(content)) {
427
+ const lNum = lineNum(check.regex);
428
+ const lines = content.split('\n');
429
+ const lineText = lines[lNum - 1] || '';
430
+ if (lineText.includes('// praxis-ignore') || lineText.includes('# praxis-ignore')) continue;
427
431
  findings.push(createFinding({
428
432
  file,
429
- line: lineNum(check.regex),
433
+ line: lNum,
430
434
  severity: check.severity,
431
435
  category: this.category,
432
436
  rule: check.rule,
package/cli/bin/praxis.js CHANGED
@@ -150,9 +150,50 @@ scan
150
150
  .option('--agentic-target <score>', 'Target security score for agentic loop', parseInt)
151
151
  .option('--hermes-only', 'Run only Hermes-relevant agents')
152
152
  .option('--fail-below <threshold>', 'Exit 1 if score is below threshold')
153
+ .option('-b, --branch <name>', 'Git branch or tag to scan (when target is a Git URL)')
154
+ .option('--depth <n>', 'Git clone depth (default: 1 for shallow clone, 0 for full history)', parseInt)
155
+ .option('--git-token <token>', 'Auth token for private Git repositories (or set PRAXIS_GIT_TOKEN / GITHUB_TOKEN)')
156
+ .option('--git-history', 'Include Git commit history secret scanning')
157
+ .option('--keep-clone', 'Keep cloned repository on disk after scan instead of deleting')
158
+ .option('--submodules', 'Recursively clone Git submodules')
153
159
  .option('-v, --verbose', 'Verbose output')
154
160
  .action(auditCommand);
155
161
 
162
+ scan
163
+ .command('git <url>')
164
+ .description('Direct remote Git repository audit: clone to secure temp workspace and scan')
165
+ .option('-b, --branch <name>', 'Git branch or tag to clone and scan')
166
+ .option('--depth <n>', 'Git clone depth (default: 1 for shallow clone)', parseInt)
167
+ .option('--git-token <token>', 'Auth token for private repositories (or set PRAXIS_GIT_TOKEN / GITHUB_TOKEN)')
168
+ .option('--git-history', 'Include Git commit history secret scanning')
169
+ .option('--keep-clone', 'Keep cloned repository on disk after scan')
170
+ .option('--submodules', 'Recursively clone Git submodules')
171
+ .option('--json', 'Output results as JSON')
172
+ .option('--sarif', 'Output results in SARIF format')
173
+ .option('--csv', 'Output results as CSV')
174
+ .option('--md', 'Output results as Markdown')
175
+ .option('--html [file]', 'HTML report path (default: praxis-report.html)')
176
+ .option('--no-deps', 'Skip dependency audit')
177
+ .option('--no-ai', 'Skip AI classification')
178
+ .option('--deep', 'LLM-powered taint analysis for critical/high findings')
179
+ .option('-v, --verbose', 'Verbose output')
180
+ .action((url, opts) => auditCommand(url, opts));
181
+
182
+ scan
183
+ .command('repo <url>')
184
+ .description('Alias for "praxis scan git <url>"')
185
+ .option('-b, --branch <name>', 'Git branch or tag to clone and scan')
186
+ .option('--depth <n>', 'Git clone depth (default: 1)', parseInt)
187
+ .option('--git-token <token>', 'Auth token for private repositories')
188
+ .option('--git-history', 'Include Git commit history secret scanning')
189
+ .option('--keep-clone', 'Keep cloned repository on disk after scan')
190
+ .option('--json', 'Output results as JSON')
191
+ .option('--sarif', 'Output results in SARIF format')
192
+ .option('--html [file]', 'HTML report path (default: praxis-report.html)')
193
+ .option('--no-deps', 'Skip dependency audit')
194
+ .option('-v, --verbose', 'Verbose output')
195
+ .action((url, opts) => auditCommand(url, opts));
196
+
156
197
  scan
157
198
  .command('secrets [path]')
158
199
  .description('Fast pattern-based secret scan only (no agents)')
@@ -162,6 +203,10 @@ scan
162
203
  .option('--sarif', 'Output results in SARIF format')
163
204
  .option('--include-tests', 'Also scan test files')
164
205
  .option('--no-cache', 'Force full rescan')
206
+ .option('-b, --branch <name>', 'Git branch or tag to scan (when target is a Git URL)')
207
+ .option('--depth <n>', 'Git clone depth (default: 1)', parseInt)
208
+ .option('--git-token <token>', 'Auth token for private Git repositories')
209
+ .option('--keep-clone', 'Keep cloned repository on disk after scan')
165
210
  .action(scanCommand);
166
211
 
167
212
  scan
@@ -45,6 +45,7 @@ import { generatePDF, generatePrintHTML, isChromeAvailable } from '../utils/pdf-
45
45
  import { SecretsVerifier } from '../utils/secrets-verifier.js';
46
46
  import { applyInlineAnnotations } from './autofix.js';
47
47
  import { buildScanFingerprint } from '../utils/scan-fingerprint.js';
48
+ import { isGitUrl, cloneGitRepo } from '../core/git-clone.js';
48
49
 
49
50
  // =============================================================================
50
51
  // CONSTANTS
@@ -83,18 +84,44 @@ const EFFORT_MAP = {
83
84
  // =============================================================================
84
85
 
85
86
  export async function auditCommand(targetPath = '.', options = {}) {
86
- const absolutePath = path.resolve(targetPath);
87
87
  const machineOutput = options.json || options.sarif || options.csv || options.md;
88
+ let gitClone = null;
89
+ let effectivePath = targetPath;
88
90
 
89
- if (!fs.existsSync(absolutePath)) {
90
- console.error(chalk.red(` Path does not exist: ${absolutePath}`));
91
- process.exitCode = 1;
92
- return;
91
+ // ── Remote Git Repository Support ─────────────────────────────────────────
92
+ if (isGitUrl(targetPath)) {
93
+ if (!machineOutput) printBanner();
94
+ const gitSpinner = machineOutput ? null : ora({ text: chalk.cyan(`Cloning remote Git repository: ${targetPath}...`), color: 'cyan' }).start();
95
+ try {
96
+ gitClone = cloneGitRepo(targetPath, {
97
+ branch: options.branch,
98
+ depth: options.depth,
99
+ gitToken: options.gitToken,
100
+ submodules: options.submodules,
101
+ gitHistory: options.gitHistory,
102
+ });
103
+ effectivePath = gitClone.tempDir;
104
+ if (gitSpinner) gitSpinner.succeed(chalk.green(`Cloned ${gitClone.repoName} (${gitClone.displayUrl}) to temporary workspace`));
105
+ } catch (err) {
106
+ if (gitSpinner) gitSpinner.fail(chalk.red(err.message));
107
+ else console.error(chalk.red(err.message));
108
+ process.exitCode = 1;
109
+ return;
110
+ }
93
111
  }
94
112
 
95
- if (!machineOutput) {
96
- printBanner();
97
- }
113
+ try {
114
+ const absolutePath = path.resolve(effectivePath);
115
+
116
+ if (!fs.existsSync(absolutePath)) {
117
+ console.error(chalk.red(` Path does not exist: ${absolutePath}`));
118
+ process.exitCode = 1;
119
+ return;
120
+ }
121
+
122
+ if (!machineOutput && !gitClone) {
123
+ printBanner();
124
+ }
98
125
 
99
126
  // ── Cache Layer ──────────────────────────────────────────────────────────
100
127
  // --deep forces a fresh scan so taint analysis covers every finding,
@@ -624,6 +651,15 @@ export async function auditCommand(targetPath = '.', options = {}) {
624
651
  }
625
652
  process.exitCode = scoreResult.score >= threshold ? 0 : 1;
626
653
  }
654
+ } finally {
655
+ if (gitClone) {
656
+ if (!options.keepClone) {
657
+ gitClone.cleanup();
658
+ } else if (!machineOutput) {
659
+ console.log(chalk.gray(`\n Cloned repository retained at: ${gitClone.tempDir}`));
660
+ }
661
+ }
662
+ }
627
663
  }
628
664
 
629
665
  /**
@@ -38,6 +38,7 @@ import {
38
38
  import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
39
39
  import * as output from '../utils/output.js';
40
40
  import { CacheManager } from '../utils/cache-manager.js';
41
+ import { isGitUrl, cloneGitRepo } from '../core/git-clone.js';
41
42
 
42
43
  // =============================================================================
43
44
  // CUSTOM PATTERNS (.praxis.json)
@@ -86,10 +87,37 @@ function loadCustomPatterns(rootPath) {
86
87
  // =============================================================================
87
88
 
88
89
  export async function scanCommand(targetPath = '.', options = {}) {
89
- const absolutePath = path.resolve(targetPath);
90
+ let gitClone = null;
91
+ let effectivePath = targetPath;
92
+
93
+ if (isGitUrl(targetPath)) {
94
+ const gitSpinner = ora({ text: chalk.cyan(`Cloning remote Git repository: ${targetPath}...`), color: 'cyan' }).start();
95
+ try {
96
+ gitClone = cloneGitRepo(targetPath, {
97
+ branch: options.branch,
98
+ depth: options.depth,
99
+ gitToken: options.gitToken,
100
+ submodules: options.submodules,
101
+ });
102
+ effectivePath = gitClone.tempDir;
103
+ gitSpinner.succeed(chalk.green(`Cloned ${gitClone.repoName} (${gitClone.displayUrl})`));
104
+ } catch (err) {
105
+ gitSpinner.fail(chalk.red(err.message));
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ const cleanup = () => {
111
+ if (gitClone && !options.keepClone) {
112
+ gitClone.cleanup();
113
+ }
114
+ };
115
+
116
+ const absolutePath = path.resolve(effectivePath);
90
117
 
91
118
  // Validate path exists
92
119
  if (!fs.existsSync(absolutePath)) {
120
+ cleanup();
93
121
  output.error(`Path does not exist: ${absolutePath}`);
94
122
  process.exit(1);
95
123
  }
@@ -209,10 +237,12 @@ export async function scanCommand(targetPath = '.', options = {}) {
209
237
  }
210
238
 
211
239
  // Exit with appropriate code
240
+ cleanup();
212
241
  const hasFindings = allResults.length > 0;
213
242
  process.exit(hasFindings ? 1 : 0);
214
243
 
215
244
  } catch (err) {
245
+ cleanup();
216
246
  spinner.fail('Scan failed');
217
247
  output.error(err.message);
218
248
  process.exit(1);
@@ -0,0 +1,210 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "generatedAt": "2026-10-02T18:49:22.592Z",
4
+ "rootPath": "C:\\STUDY\\Github\\CADRE-Platform\\Praxis\\cli\\core",
5
+ "recon": {
6
+ "frameworks": [],
7
+ "languages": [
8
+ "javascript"
9
+ ],
10
+ "apiRoutes": [],
11
+ "authPatterns": [],
12
+ "databases": [],
13
+ "cloudProviders": [],
14
+ "frontendExposure": [],
15
+ "packageManagers": [],
16
+ "cicd": [],
17
+ "hasDockerfile": false,
18
+ "hasTerraform": false,
19
+ "hasKubernetes": false,
20
+ "envFiles": [],
21
+ "configFiles": [],
22
+ "hasModelFiles": false,
23
+ "modelFiles": []
24
+ },
25
+ "fileIndex": {
26
+ "branding.js": {
27
+ "hash": "fa5bc771350efb48054cb614633a03a666b9ad5dd8d871dab41778076a71ab29",
28
+ "size": 1842,
29
+ "lastScanned": "2026-10-02T18:49:22.590Z"
30
+ },
31
+ "env.js": {
32
+ "hash": "2329f75078712d12885da3fdeed8323080102994dc1a80060c943b7e4c449f61",
33
+ "size": 1118,
34
+ "lastScanned": "2026-10-02T18:49:22.590Z"
35
+ },
36
+ "errors.js": {
37
+ "hash": "bfb1c9b324bca53a2bc52aa89e0111d0a4b66bb7758349a78f0cf9370d66197d",
38
+ "size": 1785,
39
+ "lastScanned": "2026-10-02T18:49:22.590Z"
40
+ },
41
+ "fs.js": {
42
+ "hash": "891d0ea6d876b57b9b6c87b32eaec3fa288cb02843198f00591fd5856aa01e62",
43
+ "size": 1786,
44
+ "lastScanned": "2026-10-02T18:49:22.590Z"
45
+ },
46
+ "version.js": {
47
+ "hash": "9aebd648284fa4cec1b956af4ac49be716b5aa6cf328854160a42e349c0315ac",
48
+ "size": 2489,
49
+ "lastScanned": "2026-10-02T18:49:22.590Z"
50
+ },
51
+ "ast/guardrail-detector.js": {
52
+ "hash": "d8d43bf9158eca11e3299b54547b9e77677f791e8b388878bc270e039bca9c2b",
53
+ "size": 5241,
54
+ "lastScanned": "2026-10-02T18:49:22.590Z"
55
+ },
56
+ "ast/index.js": {
57
+ "hash": "8ce6688a476826aa1a87071b6e0b6bce033e30573e4732a8fc48d5b800596012",
58
+ "size": 960,
59
+ "lastScanned": "2026-10-02T18:49:22.590Z"
60
+ },
61
+ "ast/parser.js": {
62
+ "hash": "c277d9fbf8029afdf4018a8d4794596c7ef29260e765e67fb4eb8f6930ffa99e",
63
+ "size": 21531,
64
+ "lastScanned": "2026-10-02T18:49:22.591Z"
65
+ },
66
+ "ast/scope-tree.js": {
67
+ "hash": "ab409fe692cddd921bb9733a78357b598db00da1e5ada8cb69497b416f641d77",
68
+ "size": 7126,
69
+ "lastScanned": "2026-10-02T18:49:22.591Z"
70
+ },
71
+ "ast/taint-tracker.js": {
72
+ "hash": "c225e3f85c57f4f2831300b42426d5119e612b23839f3a25e2035bf9d5417476",
73
+ "size": 6316,
74
+ "lastScanned": "2026-10-02T18:49:22.591Z"
75
+ },
76
+ "web/jobs.js": {
77
+ "hash": "91bef088c5f2d713300063ee65b0f6065e51a29432a96a7ac7e101cec05fcd19",
78
+ "size": 7025,
79
+ "lastScanned": "2026-10-02T18:49:22.591Z"
80
+ },
81
+ "web/projects.js": {
82
+ "hash": "6961f8ee3ea4a1d18620cd4d2ab802ddc5db17db106743990ac91af870b006c5",
83
+ "size": 5118,
84
+ "lastScanned": "2026-10-02T18:49:22.591Z"
85
+ },
86
+ "web/server.js": {
87
+ "hash": "fba1f8c517d3042520788336fb4efa6741dc7221fda2b04e0d58d7085323582f",
88
+ "size": 14108,
89
+ "lastScanned": "2026-10-02T18:49:22.591Z"
90
+ },
91
+ "web/ui.js": {
92
+ "hash": "c582824d031a66d3f068c594540b177c129d90259c366d522db65cd5dd41134c",
93
+ "size": 75416,
94
+ "lastScanned": "2026-10-02T18:49:22.591Z"
95
+ },
96
+ "output/compliance.js": {
97
+ "hash": "830b66d76e9029d0d569d312b5740d9d521b2408858dce519bb0048a25205ab0",
98
+ "size": 4057,
99
+ "lastScanned": "2026-10-02T18:49:22.591Z"
100
+ },
101
+ "output/html-theme.js": {
102
+ "hash": "33cacad539fb303e1e33f4cdd6868599d3824d4ea6f1d4750bdefbba21461fcd",
103
+ "size": 6823,
104
+ "lastScanned": "2026-10-02T18:49:22.591Z"
105
+ },
106
+ "output/index.js": {
107
+ "hash": "dfff64688caed4822b117bbb75ddd796666e53b2ff15b3a868abb94954d2db9a",
108
+ "size": 1503,
109
+ "lastScanned": "2026-10-02T18:49:22.592Z"
110
+ },
111
+ "output/json.js": {
112
+ "hash": "29cdf0b268b658fb24badacca9fe1a42e996acc5c25f6601ebde529a1501c8c7",
113
+ "size": 1427,
114
+ "lastScanned": "2026-10-02T18:49:22.592Z"
115
+ },
116
+ "output/sarif.js": {
117
+ "hash": "0b209b9d403e48859511880ea7caf36f8f435dafe80dc7318bc9f46bb748cc2c",
118
+ "size": 8863,
119
+ "lastScanned": "2026-10-02T18:49:22.592Z"
120
+ }
121
+ },
122
+ "lastFindings": {
123
+ "cli/__tests__/ast-engine.test.js": [
124
+ {
125
+ "line": 0,
126
+ "column": 0,
127
+ "severity": "high",
128
+ "category": "history",
129
+ "rule": "GIT_HISTORY_SECRET",
130
+ "title": "Historical Secret: Generic API Key Assignment",
131
+ "description": "Secret was removed from code but still exists in git history (commit 322700ad27, Tue Aug 18 11:19:47 2026 +0530). Anyone with repo access can retrieve it.",
132
+ "matched": "API_KEY ***123\"",
133
+ "confidence": "low",
134
+ "cwe": null,
135
+ "owasp": null,
136
+ "fix": "Rotate this credential immediately, then clean history: npx bfg --replace-text passwords.txt"
137
+ }
138
+ ],
139
+ "ast/guardrail-detector.js": [
140
+ {
141
+ "line": 70,
142
+ "column": 0,
143
+ "severity": "high",
144
+ "category": "llm",
145
+ "rule": "AI_EVAL_HARNESS_DISABLED_GUARDRAILS",
146
+ "title": "Eval Harness Disables Safety Guardrails",
147
+ "description": "An evaluation harness or agent-sandbox config disables model safety guardrails. This is the configuration class behind the OpenAI–Hugging Face July 2026 incident (models evaluated with cyber refusals disabled), and enables unchecked autonomous behavior during evaluation.",
148
+ "matched": "AI_EVAL_HARNESS_DISABLED_GUARDRAILS",
149
+ "confidence": "medium",
150
+ "cwe": "CWE-284",
151
+ "owasp": "ASI08",
152
+ "fix": "Keep safety guardrails enabled during evaluation, or gate their removal behind explicit containment controls (no internet, isolated egress)."
153
+ }
154
+ ],
155
+ "web/server.js": [
156
+ {
157
+ "line": 171,
158
+ "column": 46,
159
+ "severity": "medium",
160
+ "category": "ssrf",
161
+ "rule": "SSRF_INTERNAL_IP",
162
+ "title": "SSRF: Internal IP Pattern",
163
+ "description": "Internal IP address in code. Verify it is not reachable via user-controlled URLs.",
164
+ "matched": "127.0.0.1",
165
+ "confidence": "low",
166
+ "cwe": "CWE-918",
167
+ "owasp": "A10:2021",
168
+ "fix": "Block private IP ranges in URL validation for user-supplied URLs"
169
+ }
170
+ ],
171
+ "../../../../../../Users/Ganro/.cursor/mcp.json": [
172
+ {
173
+ "line": 1,
174
+ "column": 0,
175
+ "severity": "medium",
176
+ "category": "llm",
177
+ "rule": "MCP_SHADOW_CONFIG",
178
+ "title": "MCP: 1 Shadow Server(s) in User Config",
179
+ "description": "Found 1 MCP server(s) configured outside the project in C:\\Users\\Ganro\\.cursor\\mcp.json. These operate outside your project's security controls.",
180
+ "matched": "redstrike",
181
+ "confidence": "medium",
182
+ "cwe": "CWE-269",
183
+ "owasp": "A05:2021",
184
+ "fix": "Review shadow MCP servers. Move project-specific servers to the project mcp.json and track in version control."
185
+ }
186
+ ],
187
+ "observability": [
188
+ {
189
+ "line": 1,
190
+ "column": 0,
191
+ "severity": "medium",
192
+ "category": "llm",
193
+ "rule": "NO_OBSERVABILITY",
194
+ "title": "AI in Use Without Observability/Tracing",
195
+ "description": "This project calls LLMs, but no AI tracing/observability wiring (LangSmith, Langfuse, Helicone, OpenTelemetry, ...) was found outside dependency manifests. Without traces you cannot detect prompt-injection events, monitor output quality, or respond to AI incidents — EU AI Act Art. 12 / ISO 42001 A.6.2.6.",
196
+ "matched": "no tracing wiring in 19 evidence file(s)",
197
+ "confidence": "medium",
198
+ "cwe": "CWE-778",
199
+ "owasp": "ASI03",
200
+ "fix": "Wire an AI tracing/observability stack (e.g., Langfuse, Helicone, or OpenTelemetry) and log prompts, tool calls, and outputs with retention."
201
+ }
202
+ ]
203
+ },
204
+ "stats": {
205
+ "totalFiles": 19,
206
+ "totalFindings": 5,
207
+ "lastScore": 86.2,
208
+ "lastGrade": "B"
209
+ }
210
+ }