dscan-security 0.1.0__tar.gz

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.
@@ -0,0 +1,43 @@
1
+ # Secrets and local config
2
+ .env
3
+ .env.*
4
+ *.pem
5
+ *.key
6
+ dscan.yml # local user config — never commit
7
+
8
+ # dscan traces — never commit user data
9
+ ~/.dscan/
10
+ .dscan/
11
+ traces/
12
+
13
+ # Python
14
+ __pycache__/
15
+ *.pyc
16
+ *.py[cod]
17
+ *$py.class
18
+ .venv/
19
+ venv/
20
+ env/
21
+ build/
22
+ dist/
23
+ .eggs/
24
+ *.egg
25
+ *.egg-info/
26
+
27
+ # Tooling caches
28
+ .pytest_cache/
29
+ .coverage
30
+ .coverage.*
31
+ htmlcov/
32
+ coverage.xml
33
+ .mypy_cache/
34
+ .ruff_cache/
35
+ .uv/
36
+
37
+ # IDE
38
+ .vscode/
39
+ .idea/
40
+ .cursor/
41
+
42
+ # OS
43
+ .DS_Store
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: dscan-security
3
+ Version: 0.1.0
4
+ Summary: Open source agent security suite — intercept, scan, and test your AI agents
5
+ Project-URL: Homepage, https://deepscan.security
6
+ Project-URL: Repository, https://github.com/DeepScan-Security/dscan
7
+ Project-URL: Bug Tracker, https://github.com/DeepScan-Security/dscan/issues
8
+ Project-URL: Changelog, https://github.com/DeepScan-Security/dscan/blob/main/CHANGELOG.md
9
+ Author-email: DeepScan <security@deepscan.security>
10
+ License: MIT
11
+ Keywords: agent-security,ai-agents,devsecops,llm-security,mcp,prompt-injection,security
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: aiofiles>=23.0
22
+ Requires-Dist: aiohttp>=3.9
23
+ Requires-Dist: anthropic>=0.25
24
+ Requires-Dist: click>=8.0
25
+ Requires-Dist: jinja2>=3.0
26
+ Requires-Dist: rich>=13.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # dscan
34
+
35
+ [![CI](https://github.com/DeepScan-Security/dscan/actions/workflows/ci.yml/badge.svg)](https://github.com/DeepScan-Security/dscan/actions/workflows/ci.yml)
36
+ ![coverage](https://img.shields.io/badge/coverage-97%25-brightgreen)
37
+ [![PyPI](https://img.shields.io/pypi/v/dscan-security)](https://pypi.org/project/dscan-security/)
38
+ [![Python](https://img.shields.io/pypi/pyversions/dscan-security)](https://pypi.org/project/dscan-security/)
39
+ ![license](https://img.shields.io/badge/license-MIT-green)
40
+
41
+ An open source agent security suite. Trace and redact your agent's tool
42
+ calls, scan its prompts and MCP configs, and detect suspicious call
43
+ chains — then inspect everything in a local dashboard.
44
+
45
+ ```bash
46
+ pip install dscan-security
47
+ ```
48
+
49
+ The package installs as `dscan-security`; the import path and CLI stay
50
+ `dscan` (`import dscan`, `dscan --help`).
51
+
52
+ ![dscan dashboard](docs/dashboard.png)
53
+
54
+ ## Quick start
55
+
56
+ ```python
57
+ from dscan import watch
58
+
59
+ @watch
60
+ async def my_agent(task: str):
61
+ ... # your agent code, unchanged
62
+ ```
63
+
64
+ Run your agent, then open the dashboard:
65
+
66
+ ```bash
67
+ dscan dashboard
68
+ ```
69
+
70
+ Tool calls are written as redacted NDJSON to `~/.dscan/traces/` (override
71
+ with `DSCAN_TRACES_DIR`). Wrapping any tool with `@watch.tool` traces its
72
+ params and result; if you use the Anthropic SDK, `@watch` also intercepts
73
+ `messages.create()` and traces each `tool_use` block.
74
+
75
+ ## What dscan catches
76
+
77
+ | Capability | What it detects |
78
+ | --- | --- |
79
+ | `@watch` (tracing) | Every tool call — name, params, result, duration — written as redacted NDJSON; flags calls whose params contain secrets |
80
+ | Redaction | AWS keys, Anthropic/OpenAI/GitHub/Stripe tokens, JWTs, emails, phone numbers, SSNs, Luhn-valid credit cards, database-URL passwords, high-entropy secrets |
81
+ | `dscan scan` | Permissive prompts, injection-prone prompts, hardcoded secrets, excessive tool scope; unverified, overprivileged, and credential-leaking MCP servers |
82
+ | `dscan trail` | Suspicious tool-call sequences across an agent run (exfiltration, recon, injection relay, data staging, goal drift) |
83
+ | `dscan dashboard` | Local web UI: sessions, per-call timeline, redacted values, secrets flags, and trail findings |
84
+
85
+ ## Commands
86
+
87
+ ### `dscan scan`
88
+
89
+ Static analysis of system prompts and MCP config files in a directory.
90
+ Exits `1` if any high-severity issue is found.
91
+
92
+ ```text
93
+ $ dscan scan ./prompts
94
+ HIGH (1)
95
+ ┏━━━━━━━┳━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
96
+ ┃ Rule ┃ File ┃ Line ┃ Message ┃ Snippet ┃
97
+ ┡━━━━━━━╇━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
98
+ │ SP003 │ sp.txt │ 2 │ Hardcoded secret in prompt │ ...sk-ant-api03... │
99
+ └───────┴────────┴──────┴─────────────────────────────┴───────────────────┘
100
+ ✗ 1 high-severity finding(s).
101
+ ```
102
+
103
+ ### `dscan watch`
104
+
105
+ `@watch` is a decorator, not a runtime command. This prints a reminder:
106
+
107
+ ```text
108
+ $ dscan watch
109
+ ⚠ Add @watch to your agent function. See README for usage.
110
+ ```
111
+
112
+ ### `dscan trail`
113
+
114
+ Reads trace files and reports suspicious tool-call chains, grouped by
115
+ severity. Exits `1` on any high or critical finding. Supports
116
+ `--min-severity {low|medium|high|critical}` and `--json`.
117
+
118
+ ```text
119
+ $ dscan trail ~/.dscan/traces/
120
+ ┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
121
+ ┃ Severity ┃ Pattern ┃ Tools Involved ┃ Message ┃ Confidence ┃
122
+ ┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
123
+ │ CRITICAL │ INJECTION_RELAY │ search_web → send_email │ untrusted... │ 85% │
124
+ │ HIGH │ EXFIL_SEQUENCE │ read_file → send_email │ read then... │ 80% │
125
+ └──────────┴─────────────────┴─────────────────────────┴───────────────┴────────────┘
126
+ 2 findings across 5 tool calls analysed
127
+ ```
128
+
129
+ ### `dscan dashboard`
130
+
131
+ Serves the local web UI at `localhost:4321`. `--port` sets the port;
132
+ `--no-open` skips opening a browser.
133
+
134
+ ```text
135
+ $ dscan dashboard --no-open
136
+ ✓ Dashboard at http://127.0.0.1:4321 (Ctrl-C to stop)
137
+ ```
138
+
139
+ ## How trail works
140
+
141
+ `dscan trail` reads your trace files and looks at the order of tool
142
+ calls, not just each call on its own. It groups calls by agent run and
143
+ reports five kinds of suspicious sequences: reading sensitive data and
144
+ then sending it somewhere external (exfiltration), probing for
145
+ permissions and then running a tool the agent never declared
146
+ (reconnaissance), pulling in untrusted web or email content and then
147
+ immediately sending or executing (injection relay), reading several
148
+ different sensitive sources in a row with nothing sent in between (data
149
+ staging), and taking destructive or outbound actions that a read-only
150
+ goal never asked for (goal drift). Each finding carries a severity and a
151
+ confidence score so you can triage.
152
+
153
+ ## Contributing
154
+
155
+ ```bash
156
+ git clone https://github.com/DeepScan-Security/dscan
157
+ cd dscan
158
+ pip install -e ".[dev]"
159
+ pytest
160
+ ```
161
+
162
+ Built test-first; coverage stays at or above 80%. See
163
+ [CONTRIBUTING.md](CONTRIBUTING.md) for what we need help with, and
164
+ [SECURITY.md](SECURITY.md) to report a vulnerability.
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,136 @@
1
+ # dscan
2
+
3
+ [![CI](https://github.com/DeepScan-Security/dscan/actions/workflows/ci.yml/badge.svg)](https://github.com/DeepScan-Security/dscan/actions/workflows/ci.yml)
4
+ ![coverage](https://img.shields.io/badge/coverage-97%25-brightgreen)
5
+ [![PyPI](https://img.shields.io/pypi/v/dscan-security)](https://pypi.org/project/dscan-security/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/dscan-security)](https://pypi.org/project/dscan-security/)
7
+ ![license](https://img.shields.io/badge/license-MIT-green)
8
+
9
+ An open source agent security suite. Trace and redact your agent's tool
10
+ calls, scan its prompts and MCP configs, and detect suspicious call
11
+ chains — then inspect everything in a local dashboard.
12
+
13
+ ```bash
14
+ pip install dscan-security
15
+ ```
16
+
17
+ The package installs as `dscan-security`; the import path and CLI stay
18
+ `dscan` (`import dscan`, `dscan --help`).
19
+
20
+ ![dscan dashboard](docs/dashboard.png)
21
+
22
+ ## Quick start
23
+
24
+ ```python
25
+ from dscan import watch
26
+
27
+ @watch
28
+ async def my_agent(task: str):
29
+ ... # your agent code, unchanged
30
+ ```
31
+
32
+ Run your agent, then open the dashboard:
33
+
34
+ ```bash
35
+ dscan dashboard
36
+ ```
37
+
38
+ Tool calls are written as redacted NDJSON to `~/.dscan/traces/` (override
39
+ with `DSCAN_TRACES_DIR`). Wrapping any tool with `@watch.tool` traces its
40
+ params and result; if you use the Anthropic SDK, `@watch` also intercepts
41
+ `messages.create()` and traces each `tool_use` block.
42
+
43
+ ## What dscan catches
44
+
45
+ | Capability | What it detects |
46
+ | --- | --- |
47
+ | `@watch` (tracing) | Every tool call — name, params, result, duration — written as redacted NDJSON; flags calls whose params contain secrets |
48
+ | Redaction | AWS keys, Anthropic/OpenAI/GitHub/Stripe tokens, JWTs, emails, phone numbers, SSNs, Luhn-valid credit cards, database-URL passwords, high-entropy secrets |
49
+ | `dscan scan` | Permissive prompts, injection-prone prompts, hardcoded secrets, excessive tool scope; unverified, overprivileged, and credential-leaking MCP servers |
50
+ | `dscan trail` | Suspicious tool-call sequences across an agent run (exfiltration, recon, injection relay, data staging, goal drift) |
51
+ | `dscan dashboard` | Local web UI: sessions, per-call timeline, redacted values, secrets flags, and trail findings |
52
+
53
+ ## Commands
54
+
55
+ ### `dscan scan`
56
+
57
+ Static analysis of system prompts and MCP config files in a directory.
58
+ Exits `1` if any high-severity issue is found.
59
+
60
+ ```text
61
+ $ dscan scan ./prompts
62
+ HIGH (1)
63
+ ┏━━━━━━━┳━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
64
+ ┃ Rule ┃ File ┃ Line ┃ Message ┃ Snippet ┃
65
+ ┡━━━━━━━╇━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
66
+ │ SP003 │ sp.txt │ 2 │ Hardcoded secret in prompt │ ...sk-ant-api03... │
67
+ └───────┴────────┴──────┴─────────────────────────────┴───────────────────┘
68
+ ✗ 1 high-severity finding(s).
69
+ ```
70
+
71
+ ### `dscan watch`
72
+
73
+ `@watch` is a decorator, not a runtime command. This prints a reminder:
74
+
75
+ ```text
76
+ $ dscan watch
77
+ ⚠ Add @watch to your agent function. See README for usage.
78
+ ```
79
+
80
+ ### `dscan trail`
81
+
82
+ Reads trace files and reports suspicious tool-call chains, grouped by
83
+ severity. Exits `1` on any high or critical finding. Supports
84
+ `--min-severity {low|medium|high|critical}` and `--json`.
85
+
86
+ ```text
87
+ $ dscan trail ~/.dscan/traces/
88
+ ┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
89
+ ┃ Severity ┃ Pattern ┃ Tools Involved ┃ Message ┃ Confidence ┃
90
+ ┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
91
+ │ CRITICAL │ INJECTION_RELAY │ search_web → send_email │ untrusted... │ 85% │
92
+ │ HIGH │ EXFIL_SEQUENCE │ read_file → send_email │ read then... │ 80% │
93
+ └──────────┴─────────────────┴─────────────────────────┴───────────────┴────────────┘
94
+ 2 findings across 5 tool calls analysed
95
+ ```
96
+
97
+ ### `dscan dashboard`
98
+
99
+ Serves the local web UI at `localhost:4321`. `--port` sets the port;
100
+ `--no-open` skips opening a browser.
101
+
102
+ ```text
103
+ $ dscan dashboard --no-open
104
+ ✓ Dashboard at http://127.0.0.1:4321 (Ctrl-C to stop)
105
+ ```
106
+
107
+ ## How trail works
108
+
109
+ `dscan trail` reads your trace files and looks at the order of tool
110
+ calls, not just each call on its own. It groups calls by agent run and
111
+ reports five kinds of suspicious sequences: reading sensitive data and
112
+ then sending it somewhere external (exfiltration), probing for
113
+ permissions and then running a tool the agent never declared
114
+ (reconnaissance), pulling in untrusted web or email content and then
115
+ immediately sending or executing (injection relay), reading several
116
+ different sensitive sources in a row with nothing sent in between (data
117
+ staging), and taking destructive or outbound actions that a read-only
118
+ goal never asked for (goal drift). Each finding carries a severity and a
119
+ confidence score so you can triage.
120
+
121
+ ## Contributing
122
+
123
+ ```bash
124
+ git clone https://github.com/DeepScan-Security/dscan
125
+ cd dscan
126
+ pip install -e ".[dev]"
127
+ pytest
128
+ ```
129
+
130
+ Built test-first; coverage stays at or above 80%. See
131
+ [CONTRIBUTING.md](CONTRIBUTING.md) for what we need help with, and
132
+ [SECURITY.md](SECURITY.md) to report a vulnerability.
133
+
134
+ ## License
135
+
136
+ MIT
Binary file
@@ -0,0 +1,20 @@
1
+ """dscan — an open source agent security suite.
2
+
3
+ Wrap your agent with :func:`watch` to trace, redact, and scan its
4
+ behavior, then inspect everything in a local dashboard::
5
+
6
+ from dscan import watch
7
+
8
+ @watch
9
+ async def my_agent(task: str):
10
+ ... # your agent code unchanged
11
+
12
+ # then, from the shell:
13
+ # dscan dashboard # localhost:4321
14
+ """
15
+
16
+ from dscan.watcher import watch
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ __all__ = ["watch", "__version__"]
@@ -0,0 +1,263 @@
1
+ """dscan command-line interface.
2
+
3
+ Defines the ``dscan`` Click command group and its subcommands: ``scan``
4
+ (static prompt/MCP analysis via :mod:`dscan.scanner`), ``trail``
5
+ (call-chain detection via :mod:`dscan.trail`), ``dashboard`` (launches
6
+ :mod:`dscan.dashboard.server`), and ``watch`` (a usage reminder). All
7
+ output is rendered with ``rich``. The package entry point ``dscan``
8
+ resolves to :func:`main`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ import click
18
+ from rich.console import Console
19
+ from rich.table import Table
20
+
21
+ from dscan import __version__
22
+ from dscan.trail import TrailAnalyzer
23
+
24
+ console = Console()
25
+
26
+ _SEVERITY_ORDER = ["high", "medium", "low"]
27
+ _SEVERITY_STYLE = {"high": "bold red", "medium": "#f59e0b", "low": "cyan"}
28
+
29
+ # Trail severities, lowest to highest, plus per-row table styles.
30
+ _TRAIL_RANK = {"low": 0, "medium": 1, "high": 2, "critical": 3}
31
+ _TRAIL_DISPLAY_ORDER = ["critical", "high", "medium", "low"]
32
+ _TRAIL_ROW_STYLE = {"critical": "red", "high": "#f59e0b", "medium": "yellow", "low": None}
33
+
34
+
35
+ def _ok(message: str) -> None:
36
+ console.print(f"[green]✓[/green] {message}")
37
+
38
+
39
+ def _warn(message: str) -> None:
40
+ console.print(f"[#f59e0b]⚠[/#f59e0b] {message}")
41
+
42
+
43
+ def _err(message: str) -> None:
44
+ console.print(f"[red]✗[/red] {message}")
45
+
46
+
47
+ @click.group()
48
+ @click.version_option(__version__, prog_name="dscan")
49
+ def main() -> None:
50
+ """dscan — an open source agent security suite.
51
+
52
+ Trace and redact your agent's tool calls (@watch), statically scan
53
+ prompts and MCP configs (dscan scan), and inspect everything in a
54
+ local dashboard (dscan dashboard).
55
+ """
56
+
57
+
58
+ @main.command()
59
+ def watch() -> None:
60
+ """Show how to instrument an agent (it's a decorator, not a command)."""
61
+ _warn("Add @watch to your agent function. See README for usage.")
62
+
63
+
64
+ @main.command()
65
+ @click.argument("path", required=False, default=".")
66
+ @click.option(
67
+ "--prompt",
68
+ "prompt_file",
69
+ type=click.Path(exists=True, dir_okay=False),
70
+ default=None,
71
+ help="Scan a single system-prompt file instead of a directory.",
72
+ )
73
+ def scan(path: str, prompt_file: str | None) -> None:
74
+ """Statically analyze agent configs and system prompts."""
75
+ from dscan.scanner import scan_directory, scan_file
76
+
77
+ findings = scan_file(prompt_file) if prompt_file else scan_directory(path)
78
+ _render_findings(findings)
79
+ if any(f.severity == "high" for f in findings):
80
+ sys.exit(1)
81
+
82
+
83
+ @main.command()
84
+ @click.option("--host", default="127.0.0.1", show_default=True, help="Host to bind.")
85
+ @click.option("--port", default=4321, show_default=True, help="Port to bind.")
86
+ @click.option(
87
+ "--open/--no-open",
88
+ "open_browser",
89
+ default=True,
90
+ show_default=True,
91
+ help="Open the dashboard in a browser.",
92
+ )
93
+ def dashboard(host: str, port: int, open_browser: bool) -> None:
94
+ """Launch the local trace dashboard."""
95
+ with console.status(
96
+ f"Starting dashboard at localhost:{port}...", spinner="dots"
97
+ ):
98
+ from dscan.dashboard.server import serve
99
+
100
+ _ok(f"Dashboard at [cyan]http://{host}:{port}[/cyan] [dim](Ctrl-C to stop)[/dim]")
101
+ serve(host=host, port=port, open_browser=open_browser)
102
+
103
+
104
+ @main.command()
105
+ @click.argument("path")
106
+ @click.option(
107
+ "--min-severity",
108
+ type=click.Choice(["low", "medium", "high", "critical"]),
109
+ default="low",
110
+ show_default=True,
111
+ help="Hide findings below this severity (display only; exit code still "
112
+ "reflects any high/critical finding).",
113
+ )
114
+ @click.option(
115
+ "--json",
116
+ "as_json",
117
+ is_flag=True,
118
+ default=False,
119
+ help="Output raw JSON instead of a rich table.",
120
+ )
121
+ def trail(path: str, min_severity: str, as_json: bool) -> None:
122
+ """Detect suspicious tool-call chains (CWAT) in trace files.
123
+
124
+ PATH is a trace file (.ndjson) or a directory of trace files.
125
+ """
126
+ target = Path(path)
127
+ if not target.exists():
128
+ _err(f"path not found: {path}")
129
+ sys.exit(2)
130
+
131
+ try:
132
+ traces = _load_trace_dicts(target)
133
+ except OSError as exc: # pragma: no cover - defensive
134
+ _err(f"could not read traces from {path}: {exc}")
135
+ sys.exit(2)
136
+
137
+ # Analyze each session independently so chains never bridge unrelated
138
+ # agent runs (a read in one session + a send in another is not exfil).
139
+ analyzer = TrailAnalyzer()
140
+ all_findings: list = []
141
+ for session in _group_by_session(traces):
142
+ all_findings.extend(analyzer.analyze(session))
143
+
144
+ threshold = _TRAIL_RANK[min_severity]
145
+ shown = [f for f in all_findings if _TRAIL_RANK.get(f.severity, 0) >= threshold]
146
+ total_calls = len(traces)
147
+
148
+ if as_json:
149
+ console.print_json(data=[f.to_dict() for f in shown])
150
+ elif not all_findings:
151
+ _ok(f"No issues found in {total_calls} tool calls")
152
+ elif not shown:
153
+ console.print(
154
+ f"[dim]No findings at or above {min_severity.upper()} — "
155
+ f"{len(all_findings)} lower-severity finding(s) hidden.[/dim]"
156
+ )
157
+ else:
158
+ _render_trail(shown, total_calls)
159
+
160
+ if any(f.severity in ("high", "critical") for f in all_findings):
161
+ sys.exit(1)
162
+
163
+
164
+ def _read_ndjson(path: Path) -> list[dict]:
165
+ traces: list[dict] = []
166
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
167
+ line = line.strip()
168
+ if not line:
169
+ continue
170
+ try:
171
+ obj = json.loads(line)
172
+ except (json.JSONDecodeError, ValueError):
173
+ continue # skip malformed lines
174
+ if isinstance(obj, dict):
175
+ traces.append(obj)
176
+ return traces
177
+
178
+
179
+ def _load_trace_dicts(target: Path) -> list[dict]:
180
+ files = [target] if target.is_file() else sorted(target.glob("*.ndjson"))
181
+ traces: list[dict] = []
182
+ for file in files:
183
+ traces.extend(_read_ndjson(file))
184
+ return traces
185
+
186
+
187
+ def _group_by_session(traces: list[dict]) -> list[list[dict]]:
188
+ groups: dict[str, list[dict]] = {}
189
+ order: list[str] = []
190
+ for trace in traces:
191
+ sid = str(trace.get("session_id") or "")
192
+ if sid not in groups:
193
+ groups[sid] = []
194
+ order.append(sid)
195
+ groups[sid].append(trace)
196
+ return [
197
+ sorted(groups[sid], key=lambda t: str(t.get("ts") or "")) for sid in order
198
+ ]
199
+
200
+
201
+ def _render_trail(findings: list, total_calls: int) -> None:
202
+ table = Table(header_style="bold")
203
+ table.add_column("Severity")
204
+ table.add_column("Pattern")
205
+ table.add_column("Tools Involved")
206
+ table.add_column("Message")
207
+ table.add_column("Confidence", justify="right")
208
+ for severity in _TRAIL_DISPLAY_ORDER:
209
+ for f in (x for x in findings if x.severity == severity):
210
+ table.add_row(
211
+ severity.upper(),
212
+ f.pattern,
213
+ " → ".join(f.calls_involved),
214
+ f.message,
215
+ f"{round(f.confidence * 100)}%",
216
+ style=_TRAIL_ROW_STYLE.get(severity),
217
+ )
218
+ console.print(table)
219
+ console.print(
220
+ f"[bold]{len(findings)}[/bold] findings across "
221
+ f"[bold]{total_calls}[/bold] tool calls analysed"
222
+ )
223
+
224
+
225
+ def _render_findings(findings: list) -> None:
226
+ if not findings:
227
+ _ok("No findings.")
228
+ return
229
+
230
+ for severity in _SEVERITY_ORDER:
231
+ group = [f for f in findings if f.severity == severity]
232
+ if not group:
233
+ continue
234
+ table = Table(
235
+ title=f"{severity.upper()} ({len(group)})",
236
+ title_style=_SEVERITY_STYLE[severity],
237
+ header_style="bold",
238
+ title_justify="left",
239
+ )
240
+ table.add_column("Rule")
241
+ table.add_column("File")
242
+ table.add_column("Line", justify="right")
243
+ table.add_column("Message")
244
+ table.add_column("Snippet")
245
+ for f in sorted(group, key=lambda x: (x.file, x.line, x.rule)):
246
+ table.add_row(
247
+ f.rule,
248
+ Path(f.file).name,
249
+ str(f.line),
250
+ f.message,
251
+ f.snippet,
252
+ )
253
+ console.print(table)
254
+
255
+ high = sum(1 for f in findings if f.severity == "high")
256
+ if high:
257
+ _err(f"{high} high-severity finding(s).")
258
+ else:
259
+ _warn(f"{len(findings)} finding(s), none high severity.")
260
+
261
+
262
+ if __name__ == "__main__":
263
+ main()
@@ -0,0 +1,10 @@
1
+ """dscan dashboard — local web UI for inspecting agent traces.
2
+
3
+ This package holds the dashboard's aiohttp server (:mod:`dscan.dashboard.server`)
4
+ and its single HTML template. The server reads NDJSON traces from
5
+ ``~/.dscan/traces`` (or ``DSCAN_TRACES_DIR``) and exposes them at
6
+ ``localhost:4321`` over a small JSON API plus a self-contained page that
7
+ renders sessions, redacted tool calls, and trail findings. It depends
8
+ only on ``aiohttp`` and ``aiofiles``; there are no external front-end
9
+ dependencies.
10
+ """