ecdat 0.2.0__py3-none-any.whl

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.
Files changed (51) hide show
  1. ecdat/__init__.py +10 -0
  2. ecdat/__main__.py +5 -0
  3. ecdat/cli/__init__.py +203 -0
  4. ecdat/cli/commands/__init__.py +0 -0
  5. ecdat/cli/commands/about.py +130 -0
  6. ecdat/cli/commands/demo.py +116 -0
  7. ecdat/cli/commands/doctor.py +296 -0
  8. ecdat/cli/commands/help_cmd.py +205 -0
  9. ecdat/cli/commands/scan.py +228 -0
  10. ecdat/cli/commands/version_cmd.py +48 -0
  11. ecdat/cli/parser.py +87 -0
  12. ecdat/demo_project/auth/login.py +75 -0
  13. ecdat/demo_project/certs/cert_verify.go +81 -0
  14. ecdat/demo_project/keyexchange/channel.go +48 -0
  15. ecdat/demo_project/legacy/LegacyCrypto.java +78 -0
  16. ecdat/demo_project/payments/payment.py +64 -0
  17. ecdat/demo_project/quantum/pqc_utils.py +67 -0
  18. ecdat/demo_project/quantum/slh_signer.py +40 -0
  19. ecdat/demo_project/tokens/signing.js +54 -0
  20. ecdat/py.typed +0 -0
  21. ecdat/services/__init__.py +1 -0
  22. ecdat/services/crashlog.py +109 -0
  23. ecdat/services/demo.py +85 -0
  24. ecdat/services/paths.py +52 -0
  25. ecdat/services/scanner.py +248 -0
  26. ecdat/services/viewmodel.py +326 -0
  27. ecdat/ui/__init__.py +1 -0
  28. ecdat/ui/art3d.py +136 -0
  29. ecdat/ui/art_static.py +65 -0
  30. ecdat/ui/art_text.py +81 -0
  31. ecdat/ui/banner.py +148 -0
  32. ecdat/ui/console.py +119 -0
  33. ecdat/ui/motion.py +64 -0
  34. ecdat/ui/render.py +486 -0
  35. ecdat/ui/theme.py +173 -0
  36. ecdat-0.2.0.dist-info/METADATA +142 -0
  37. ecdat-0.2.0.dist-info/RECORD +51 -0
  38. ecdat-0.2.0.dist-info/WHEEL +5 -0
  39. ecdat-0.2.0.dist-info/entry_points.txt +2 -0
  40. ecdat-0.2.0.dist-info/licenses/LICENSE +21 -0
  41. ecdat-0.2.0.dist-info/top_level.txt +2 -0
  42. ecdat_core/__init__.py +6 -0
  43. ecdat_core/cbom_export.py +287 -0
  44. ecdat_core/cli.py +202 -0
  45. ecdat_core/detector.py +273 -0
  46. ecdat_core/ingestion.py +581 -0
  47. ecdat_core/models.py +145 -0
  48. ecdat_core/recommender.py +74 -0
  49. ecdat_core/risk_engine.py +264 -0
  50. ecdat_core/signature_loader.py +204 -0
  51. ecdat_core/signatures.json +692 -0
@@ -0,0 +1,296 @@
1
+ """``ecdat doctor`` — environment self-check.
2
+
3
+ Runs a small set of independent checks and prints one row per check with a
4
+ status glyph, a short value, and a one-line fix when something needs
5
+ attention. The output is plain and copy-pasteable into a GitHub issue.
6
+ Exits ``1`` only when at least one check genuinely fails (``\u2717``);
7
+ warnings (``!``) never fail the command.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import os
14
+ import platform
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ NAME = "doctor"
23
+
24
+ _OK = "\u2713" # ✓
25
+ _WARN = "!" # !
26
+ _FAIL = "\u2717" # ✗
27
+
28
+ # Minimum interpreter the app layer supports (see AGENTS.md: 3.10-compatible code).
29
+ _MIN_PYTHON = (3, 10)
30
+
31
+ _ENV_NAMES = ("ECDAT_HOME", "ECDAT_ANIM", "NO_COLOR")
32
+
33
+
34
+ def register(subparsers: "argparse._SubParsersAction") -> None:
35
+ """Attach the ``doctor`` subcommand to *subparsers*."""
36
+ subparsers.add_parser(
37
+ "doctor",
38
+ help="Check your environment for common problems",
39
+ description=(
40
+ "Check Python, platform, install location, git, terminal, "
41
+ "signatures, ECDAT_HOME, and environment overrides. Every row "
42
+ "carries a one-line fix. Exits 1 only when a check fails."
43
+ ),
44
+ )
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class Check:
49
+ """A single doctor check result.
50
+
51
+ Attributes:
52
+ status: ``"ok"``, ``"warn"``, or ``"fail"``.
53
+ label: Short check name (e.g. ``"Python"``).
54
+ detail: Human-readable value for the check.
55
+ fix: One-line remediation hint (empty when no fix is needed).
56
+ """
57
+
58
+ status: str
59
+ label: str
60
+ detail: str
61
+ fix: str = ""
62
+
63
+
64
+ def run(args) -> int:
65
+ """Run all checks, print the report, and return the exit code."""
66
+ from rich.style import Style
67
+ from rich.table import Table
68
+ from rich.text import Text
69
+
70
+ from ecdat.ui.console import make_console
71
+ from ecdat.ui.theme import PALETTE
72
+
73
+ no_color = True if getattr(args, "no_color", False) else None
74
+ console = make_console(no_color=no_color)
75
+
76
+ checks = _run_checks(console)
77
+
78
+ table = Table(box=None, show_header=False, padding=(0, 2), expand=False)
79
+ table.add_column(no_wrap=True)
80
+ table.add_column(no_wrap=True)
81
+ table.add_column(overflow="fold")
82
+
83
+ for check in checks:
84
+ table.add_row(
85
+ _status_text(check.status),
86
+ Text(check.label),
87
+ _detail_text(check),
88
+ )
89
+
90
+ console.print(
91
+ Text(
92
+ "ECDAT doctor \u2014 environment report",
93
+ style=Style(color=PALETTE.accent, bold=True),
94
+ )
95
+ )
96
+ console.print(table)
97
+
98
+ failed = [c for c in checks if c.status == "fail"]
99
+ warned = [c for c in checks if c.status == "warn"]
100
+ if failed:
101
+ console.print(
102
+ Text(
103
+ f"{len(failed)} check(s) failed \u2014 see fixes above.",
104
+ style=Style(color=PALETTE.critical, bold=True),
105
+ )
106
+ )
107
+ return 1
108
+ if warned:
109
+ console.print(
110
+ Text(
111
+ "No blocking problems found.",
112
+ style=Style(color=PALETTE.safe),
113
+ )
114
+ )
115
+ return 0
116
+ console.print(Text("All checks passed.", style=Style(color=PALETTE.safe)))
117
+ return 0
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Individual checks
122
+ # ---------------------------------------------------------------------------
123
+
124
+
125
+ def _run_checks(console) -> list[Check]:
126
+ """Collect every environment check, ordered for readability."""
127
+ return [
128
+ _check_python(),
129
+ _check_platform(),
130
+ _check_install_location(),
131
+ _check_git(),
132
+ _check_terminal(console),
133
+ _check_signatures(),
134
+ _check_home_writable(),
135
+ _check_env_overrides(),
136
+ ]
137
+
138
+
139
+ def _check_python() -> Check:
140
+ version = platform.python_version()
141
+ impl = platform.python_implementation()
142
+ if sys.version_info >= _MIN_PYTHON:
143
+ return Check("ok", "Python", f"{version} ({impl})")
144
+ requirement = ".".join(str(part) for part in _MIN_PYTHON)
145
+ return Check(
146
+ "fail",
147
+ "Python",
148
+ f"{version} ({impl}) \u2014 too old",
149
+ f"Install Python {requirement} or newer.",
150
+ )
151
+
152
+
153
+ def _check_platform() -> Check:
154
+ system = platform.system() or "unknown"
155
+ release = platform.release() or "?"
156
+ machine = platform.machine() or "?"
157
+ return Check("ok", "Platform", f"{system} {release} ({machine})")
158
+
159
+
160
+ def _check_install_location() -> Check:
161
+ import ecdat
162
+
163
+ location = Path(ecdat.__file__).resolve().parent
164
+ return Check("ok", "Install", str(location))
165
+
166
+
167
+ def _check_git() -> Check:
168
+ git_path = shutil.which("git")
169
+ if git_path is None:
170
+ return Check(
171
+ "warn",
172
+ "git",
173
+ "not found on PATH",
174
+ "Install git to enable repository URL scans.",
175
+ )
176
+ try:
177
+ proc = subprocess.run(
178
+ [git_path, "--version"],
179
+ capture_output=True,
180
+ text=True,
181
+ timeout=5,
182
+ )
183
+ output = (proc.stdout or proc.stderr).strip()
184
+ version = output.splitlines()[0] if output else "present"
185
+ if proc.returncode != 0:
186
+ return Check(
187
+ "warn",
188
+ "git",
189
+ f"{git_path} (version check exited {proc.returncode})",
190
+ "Verify git runs: git --version",
191
+ )
192
+ return Check("ok", "git", f"{version} ({git_path})")
193
+ except (OSError, subprocess.SubprocessError) as exc:
194
+ return Check(
195
+ "warn",
196
+ "git",
197
+ f"{git_path} (version check failed: {exc})",
198
+ "Verify git runs: git --version",
199
+ )
200
+
201
+
202
+ def _check_terminal(console) -> Check:
203
+ try:
204
+ size = console.size
205
+ dimensions = f"{size.width}x{size.height}"
206
+ except Exception: # noqa: BLE001 - size is best-effort diagnostics
207
+ dimensions = "?"
208
+ encoding = sys.stdout.encoding or "?"
209
+ color_system = console.color_system or "none"
210
+ term = os.environ.get("TERM") or "(unset)"
211
+ detail = (
212
+ f"interactive={sys.stdout.isatty()}, size={dimensions}, "
213
+ f"color={color_system}, encoding={encoding}, TERM={term}"
214
+ )
215
+ return Check("ok", "Terminal", detail)
216
+
217
+
218
+ def _check_signatures() -> Check:
219
+ try:
220
+ from ecdat_core.signature_loader import get_all_signatures
221
+
222
+ count = len(get_all_signatures())
223
+ except Exception as exc: # noqa: BLE001 - report any load failure verbatim
224
+ return Check(
225
+ "fail",
226
+ "signatures",
227
+ f"failed to load: {exc}",
228
+ "Reinstall ECDAT: pip install --force-reinstall ecdat",
229
+ )
230
+ return Check("ok", "signatures", f"{count} entries loaded")
231
+
232
+
233
+ def _check_home_writable() -> Check:
234
+ from ecdat.services.paths import home_dir
235
+
236
+ home = home_dir()
237
+ try:
238
+ home.mkdir(parents=True, exist_ok=True)
239
+ fd, tmp_name = tempfile.mkstemp(dir=str(home), prefix=".ecdat-write-test-")
240
+ os.close(fd)
241
+ try:
242
+ os.unlink(tmp_name)
243
+ except OSError:
244
+ pass
245
+ except OSError as exc:
246
+ return Check(
247
+ "fail",
248
+ "ECDAT_HOME",
249
+ f"{home} is not writable: {exc}",
250
+ "Set ECDAT_HOME to a writable directory.",
251
+ )
252
+ return Check("ok", "ECDAT_HOME", f"{home} (writable)")
253
+
254
+
255
+ def _check_env_overrides() -> Check:
256
+ parts = []
257
+ for name in _ENV_NAMES:
258
+ value = os.environ.get(name)
259
+ parts.append(f"{name}={value}" if value else f"{name} (unset)")
260
+ return Check("ok", "env", ", ".join(parts))
261
+
262
+
263
+ # ---------------------------------------------------------------------------
264
+ # Rendering helpers
265
+ # ---------------------------------------------------------------------------
266
+
267
+
268
+ def _status_text(status: str):
269
+ """Return the coloured status glyph for *status*."""
270
+ from rich.style import Style
271
+ from rich.text import Text
272
+
273
+ from ecdat.ui.theme import PALETTE
274
+
275
+ if status == "fail":
276
+ return Text(_FAIL, style=Style(color=PALETTE.critical, bold=True))
277
+ if status == "warn":
278
+ return Text(_WARN, style=Style(color=PALETTE.medium, bold=True))
279
+ return Text(_OK, style=Style(color=PALETTE.safe, bold=True))
280
+
281
+
282
+ def _detail_text(check: Check):
283
+ """Return the detail string plus an optional dimmed fix hint."""
284
+ from rich.style import Style
285
+ from rich.text import Text
286
+
287
+ from ecdat.ui.theme import PALETTE
288
+
289
+ text = Text(check.detail)
290
+ if check.fix:
291
+ text.append(" \u2192 ")
292
+ text.append(
293
+ check.fix,
294
+ style=Style(color=PALETTE.muted, italic=True),
295
+ )
296
+ return text
@@ -0,0 +1,205 @@
1
+ """``ecdat help`` — a friendly overview of every command.
2
+
3
+ The command table is built by introspecting the argument parser, so a newly
4
+ registered subcommand shows up here automatically. ``ecdat help <command>``
5
+ renders that subcommand's own ``argparse`` help inside a panel (as plain
6
+ :class:`rich.text.Text`, never markup).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+
13
+ NAME = "help"
14
+
15
+ _QUICK_START = (
16
+ "ecdat demo",
17
+ "ecdat scan . --fail-on high",
18
+ "ecdat scan https://github.com/acme/app --git-url",
19
+ "ecdat help scan",
20
+ )
21
+
22
+ # Optional per-command examples; unknown/new commands fall back to "ecdat <name>".
23
+ _EXAMPLES: dict[str, str] = {
24
+ "scan": "ecdat scan . --fail-on high",
25
+ "demo": "ecdat demo",
26
+ "help": "ecdat help scan",
27
+ "doctor": "ecdat doctor",
28
+ "about": "ecdat about",
29
+ "version": "ecdat version",
30
+ }
31
+
32
+ _EXIT_CODES = (
33
+ ("0", "success"),
34
+ ("1", "findings at or above --fail-on"),
35
+ ("2", "usage or validation error"),
36
+ ("3", "scan, runtime, or unexpected error"),
37
+ ("130", "interrupted (Ctrl-C)"),
38
+ )
39
+
40
+
41
+ def register(subparsers: "argparse._SubParsersAction") -> None:
42
+ """Attach the ``help`` subcommand to *subparsers*."""
43
+ parser = subparsers.add_parser(
44
+ "help",
45
+ help="List all commands, or show detailed help for one",
46
+ description=(
47
+ "List every ECDAT command with an example, or show the full "
48
+ "argparse help for a single command: ecdat help <command>."
49
+ ),
50
+ )
51
+ parser.add_argument(
52
+ "topic",
53
+ nargs="?",
54
+ default=None,
55
+ metavar="COMMAND",
56
+ help="Command to show detailed help for",
57
+ )
58
+
59
+
60
+ def run(args) -> int:
61
+ """Print the command overview or one command's help; return the exit code."""
62
+ from rich.panel import Panel
63
+ from rich.style import Style
64
+ from rich.text import Text
65
+
66
+ from ecdat import __version__
67
+ from ecdat.cli.parser import build_parser
68
+ from ecdat.ui.banner import render_banner
69
+ from ecdat.ui.console import make_console
70
+ from ecdat.ui.theme import PALETTE
71
+
72
+ no_color = True if getattr(args, "no_color", False) else None
73
+ out_console = make_console(no_color=no_color)
74
+ err_console = make_console(stderr=True, no_color=no_color)
75
+
76
+ parser = build_parser()
77
+ commands, choices = _registry(parser)
78
+ topic = getattr(args, "topic", None)
79
+
80
+ if topic is None:
81
+ out_console.print(render_banner(version=__version__))
82
+ out_console.print(Text())
83
+ out_console.print(_commands_table(commands))
84
+ out_console.print(Text())
85
+ out_console.print(
86
+ Text("Quick start", style=Style(color=PALETTE.accent, bold=True))
87
+ )
88
+ for line in _QUICK_START:
89
+ out_console.print(Text(f" {line}"))
90
+ out_console.print(Text())
91
+ out_console.print(_exit_codes_table())
92
+ return 0
93
+
94
+ if topic not in choices:
95
+ valid = ", ".join(name for name, _ in commands)
96
+ err_console.print(
97
+ Text(
98
+ f"Error: unknown command {topic!r}. Valid commands: {valid}",
99
+ style=Style(color=PALETTE.critical, bold=True),
100
+ )
101
+ )
102
+ return 2
103
+
104
+ out_console.print(
105
+ Panel(
106
+ Text(choices[topic].format_help()),
107
+ title=f"ecdat {topic}",
108
+ title_align="left",
109
+ border_style=PALETTE.border,
110
+ padding=(1, 2),
111
+ )
112
+ )
113
+ return 0
114
+
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # Parser introspection
118
+ # ---------------------------------------------------------------------------
119
+
120
+
121
+ def _registry(
122
+ parser: argparse.ArgumentParser,
123
+ ) -> tuple[list[tuple[str, str]], dict[str, argparse.ArgumentParser]]:
124
+ """Return ``[(name, summary), ...]`` and ``name -> subparser`` for *parser*.
125
+
126
+ Reads argparse's subparser action so the help table never drifts from the
127
+ real command set.
128
+ """
129
+ action = next(
130
+ (a for a in parser._actions if isinstance(a, argparse._SubParsersAction)),
131
+ None,
132
+ )
133
+ if action is None: # pragma: no cover - build_parser always adds subparsers
134
+ return [], {}
135
+
136
+ rows: list[tuple[str, str]] = []
137
+ for choice in getattr(action, "_choices_actions", []):
138
+ rows.append((choice.dest, choice.help or ""))
139
+ return rows, dict(action.choices)
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Renderers (Rich imported lazily by the callers above)
144
+ # ---------------------------------------------------------------------------
145
+
146
+
147
+ def _commands_table(commands: list[tuple[str, str]]):
148
+ """Build the rounded command/example table."""
149
+ from rich import box
150
+ from rich.style import Style
151
+ from rich.table import Table
152
+ from rich.text import Text
153
+
154
+ from ecdat.ui.theme import PALETTE
155
+
156
+ table = Table(
157
+ title="Commands",
158
+ title_style=f"bold {PALETTE.accent}",
159
+ title_justify="left",
160
+ border_style=PALETTE.border,
161
+ header_style=Style(color=PALETTE.muted, bold=True),
162
+ box=box.ROUNDED,
163
+ expand=False,
164
+ )
165
+ table.add_column("Command", no_wrap=True)
166
+ table.add_column("Summary")
167
+ table.add_column("Example", no_wrap=True)
168
+
169
+ for name, summary in commands:
170
+ example = _EXAMPLES.get(name, f"ecdat {name}")
171
+ table.add_row(
172
+ Text(f"ecdat {name}", style=Style(color=PALETTE.accent, bold=True)),
173
+ Text(summary),
174
+ Text(example),
175
+ )
176
+ return table
177
+
178
+
179
+ def _exit_codes_table():
180
+ """Build the rounded exit-code reference table."""
181
+ from rich import box
182
+ from rich.style import Style
183
+ from rich.table import Table
184
+ from rich.text import Text
185
+
186
+ from ecdat.ui.theme import PALETTE
187
+
188
+ table = Table(
189
+ title="Exit codes",
190
+ title_style=f"bold {PALETTE.accent}",
191
+ title_justify="left",
192
+ border_style=PALETTE.border,
193
+ header_style=Style(color=PALETTE.muted, bold=True),
194
+ box=box.ROUNDED,
195
+ expand=False,
196
+ )
197
+ table.add_column("Code", no_wrap=True)
198
+ table.add_column("Meaning")
199
+
200
+ for code, meaning in _EXIT_CODES:
201
+ table.add_row(
202
+ Text(code, style=Style(color=PALETTE.accent, bold=True)),
203
+ Text(meaning),
204
+ )
205
+ return table