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.
- ecdat/__init__.py +10 -0
- ecdat/__main__.py +5 -0
- ecdat/cli/__init__.py +203 -0
- ecdat/cli/commands/__init__.py +0 -0
- ecdat/cli/commands/about.py +130 -0
- ecdat/cli/commands/demo.py +116 -0
- ecdat/cli/commands/doctor.py +296 -0
- ecdat/cli/commands/help_cmd.py +205 -0
- ecdat/cli/commands/scan.py +228 -0
- ecdat/cli/commands/version_cmd.py +48 -0
- ecdat/cli/parser.py +87 -0
- ecdat/demo_project/auth/login.py +75 -0
- ecdat/demo_project/certs/cert_verify.go +81 -0
- ecdat/demo_project/keyexchange/channel.go +48 -0
- ecdat/demo_project/legacy/LegacyCrypto.java +78 -0
- ecdat/demo_project/payments/payment.py +64 -0
- ecdat/demo_project/quantum/pqc_utils.py +67 -0
- ecdat/demo_project/quantum/slh_signer.py +40 -0
- ecdat/demo_project/tokens/signing.js +54 -0
- ecdat/py.typed +0 -0
- ecdat/services/__init__.py +1 -0
- ecdat/services/crashlog.py +109 -0
- ecdat/services/demo.py +85 -0
- ecdat/services/paths.py +52 -0
- ecdat/services/scanner.py +248 -0
- ecdat/services/viewmodel.py +326 -0
- ecdat/ui/__init__.py +1 -0
- ecdat/ui/art3d.py +136 -0
- ecdat/ui/art_static.py +65 -0
- ecdat/ui/art_text.py +81 -0
- ecdat/ui/banner.py +148 -0
- ecdat/ui/console.py +119 -0
- ecdat/ui/motion.py +64 -0
- ecdat/ui/render.py +486 -0
- ecdat/ui/theme.py +173 -0
- ecdat-0.2.0.dist-info/METADATA +142 -0
- ecdat-0.2.0.dist-info/RECORD +51 -0
- ecdat-0.2.0.dist-info/WHEEL +5 -0
- ecdat-0.2.0.dist-info/entry_points.txt +2 -0
- ecdat-0.2.0.dist-info/licenses/LICENSE +21 -0
- ecdat-0.2.0.dist-info/top_level.txt +2 -0
- ecdat_core/__init__.py +6 -0
- ecdat_core/cbom_export.py +287 -0
- ecdat_core/cli.py +202 -0
- ecdat_core/detector.py +273 -0
- ecdat_core/ingestion.py +581 -0
- ecdat_core/models.py +145 -0
- ecdat_core/recommender.py +74 -0
- ecdat_core/risk_engine.py +264 -0
- ecdat_core/signature_loader.py +204 -0
- ecdat_core/signatures.json +692 -0
ecdat/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""ECDAT — Enterprise Cryptographic Discovery & Analysis Tool."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
|
|
5
|
+
DIST_NAME = "ecdat"
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = importlib.metadata.version(DIST_NAME)
|
|
9
|
+
except importlib.metadata.PackageNotFoundError:
|
|
10
|
+
__version__ = "0+unknown"
|
ecdat/__main__.py
ADDED
ecdat/cli/__init__.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""ECDAT CLI — entry point and command dispatch.
|
|
2
|
+
|
|
3
|
+
:func:`main` is the single guard rail for the whole CLI: it converts known
|
|
4
|
+
user errors (:class:`~ecdat.services.scanner.ScanError`), interrupts, and
|
|
5
|
+
broken pipes into clean exit codes, and turns genuinely unexpected exceptions
|
|
6
|
+
into a crash-log entry instead of a bare traceback.
|
|
7
|
+
|
|
8
|
+
Importing this module stays cheap — Rich, the scanner engine, and the crash
|
|
9
|
+
logger are all imported lazily. ``textual`` is never imported here (the TUI
|
|
10
|
+
is loaded by its own command, later).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from ecdat.cli.parser import COMMANDS, build_parser
|
|
20
|
+
|
|
21
|
+
__all__ = ["main"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv: Optional[list] = None) -> int:
|
|
25
|
+
"""Run the ECDAT CLI and return a process exit code.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
argv: Argument list; defaults to ``sys.argv[1:]``.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
``0`` on success, ``1`` when ``--fail-on`` is tripped, ``2`` on
|
|
32
|
+
usage/validation errors, ``3`` on unexpected errors, ``130`` on
|
|
33
|
+
``Ctrl-C``.
|
|
34
|
+
"""
|
|
35
|
+
if argv is None:
|
|
36
|
+
argv = list(sys.argv[1:])
|
|
37
|
+
|
|
38
|
+
parser = build_parser()
|
|
39
|
+
try:
|
|
40
|
+
args = parser.parse_args(argv)
|
|
41
|
+
except SystemExit as exc:
|
|
42
|
+
# argparse handles --help (0) and usage errors (2) itself.
|
|
43
|
+
return _system_exit_code(exc)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
if getattr(args, "version", False):
|
|
47
|
+
_print_version()
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
command = getattr(args, "command", None)
|
|
51
|
+
if not command:
|
|
52
|
+
return _run_bare(args)
|
|
53
|
+
|
|
54
|
+
module = COMMANDS.get(command)
|
|
55
|
+
if module is None:
|
|
56
|
+
_print_error(args, f"Unknown command: {command}")
|
|
57
|
+
return 2
|
|
58
|
+
return module.run(args)
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
return 130
|
|
61
|
+
except BrokenPipeError:
|
|
62
|
+
_silence_stdout()
|
|
63
|
+
return 0
|
|
64
|
+
except Exception as exc: # noqa: BLE001 - top-level guard rail
|
|
65
|
+
return _handle_exception(exc, argv, args)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Exit-code / error handling
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _system_exit_code(exc: SystemExit) -> int:
|
|
74
|
+
"""Map an argparse :class:`SystemExit` to a plain integer exit code."""
|
|
75
|
+
code = exc.code
|
|
76
|
+
if code is None:
|
|
77
|
+
return 0
|
|
78
|
+
if isinstance(code, int):
|
|
79
|
+
return code
|
|
80
|
+
return 2
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _handle_exception(exc: Exception, argv: list, args) -> int:
|
|
84
|
+
"""Route an exception to the right handler; return the exit code."""
|
|
85
|
+
from ecdat.services.scanner import ScanError
|
|
86
|
+
|
|
87
|
+
if isinstance(exc, ScanError):
|
|
88
|
+
_print_scan_error(args, exc)
|
|
89
|
+
return exc.exit_code
|
|
90
|
+
return _handle_unexpected(exc, argv, args)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _handle_unexpected(exc: Exception, argv: list, args) -> int:
|
|
94
|
+
"""Record an unexpected crash and report a friendly message; return 3."""
|
|
95
|
+
from ecdat.services.crashlog import write_crash_log
|
|
96
|
+
from rich.text import Text
|
|
97
|
+
|
|
98
|
+
log_path = write_crash_log(exc, argv)
|
|
99
|
+
details = str(log_path) if log_path is not None else "(crash log unavailable)"
|
|
100
|
+
|
|
101
|
+
console = _console(stderr=True, args=args)
|
|
102
|
+
console.print(
|
|
103
|
+
Text(
|
|
104
|
+
f"ECDAT hit an unexpected error. Details: {details}. "
|
|
105
|
+
f"Re-run with --debug for the traceback and attach "
|
|
106
|
+
f"`ecdat doctor` output to an issue.",
|
|
107
|
+
style="bold red",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if _is_debug(args):
|
|
112
|
+
import traceback
|
|
113
|
+
|
|
114
|
+
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
|
115
|
+
|
|
116
|
+
return 3
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _print_scan_error(args, exc) -> None:
|
|
120
|
+
"""Print a known :class:`ScanError` (plus optional hint) to stderr."""
|
|
121
|
+
from rich.text import Text
|
|
122
|
+
|
|
123
|
+
console = _console(stderr=True, args=args)
|
|
124
|
+
console.print(Text(f"Error: {exc.user_message}", style="bold red"))
|
|
125
|
+
if exc.hint:
|
|
126
|
+
console.print(Text(f"Hint: {exc.hint}", style="dim"))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _print_error(args, message: str) -> None:
|
|
130
|
+
"""Print a plain error line to stderr."""
|
|
131
|
+
from rich.text import Text
|
|
132
|
+
|
|
133
|
+
_console(stderr=True, args=args).print(Text(f"Error: {message}", style="bold red"))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _print_version() -> None:
|
|
137
|
+
"""Write ``ecdat <version>`` to stdout."""
|
|
138
|
+
from ecdat import __version__
|
|
139
|
+
|
|
140
|
+
sys.stdout.write(f"ecdat {__version__}\n")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Bare invocation
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _run_bare(args) -> int:
|
|
149
|
+
"""Print the banner and example commands (no subcommand given)."""
|
|
150
|
+
from rich.text import Text
|
|
151
|
+
|
|
152
|
+
from ecdat import __version__
|
|
153
|
+
from ecdat.ui.banner import render_banner
|
|
154
|
+
|
|
155
|
+
console = _console(args=args)
|
|
156
|
+
console.print(render_banner(version=__version__))
|
|
157
|
+
console.print(Text(""))
|
|
158
|
+
console.print(Text("Try one of these:", style="bold"))
|
|
159
|
+
for example in ("ecdat demo", "ecdat scan .", "ecdat about", "ecdat help"):
|
|
160
|
+
console.print(Text(f" {example}"))
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
# Small utilities
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _is_debug(args) -> bool:
|
|
170
|
+
"""Return ``True`` when ``--debug`` or ``ECDAT_DEBUG=1`` is set."""
|
|
171
|
+
if args is not None and getattr(args, "debug", False):
|
|
172
|
+
return True
|
|
173
|
+
return os.environ.get("ECDAT_DEBUG", "").strip() == "1"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _console(*, stderr: bool = False, args=None):
|
|
177
|
+
"""Create a themed Rich console via the sanctioned factory."""
|
|
178
|
+
from ecdat.ui.console import make_console
|
|
179
|
+
|
|
180
|
+
no_color = None
|
|
181
|
+
if args is not None and getattr(args, "no_color", False):
|
|
182
|
+
no_color = True
|
|
183
|
+
return make_console(stderr=stderr, no_color=no_color)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _silence_stdout() -> None:
|
|
187
|
+
"""Redirect the real stdout fd to ``/dev/null`` after a broken pipe.
|
|
188
|
+
|
|
189
|
+
Prevents Python's interpreter-shutdown flush from re-raising on a pipe
|
|
190
|
+
that a downstream reader (e.g. ``head``) already closed. Best-effort.
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
194
|
+
try:
|
|
195
|
+
os.dup2(devnull, sys.stdout.fileno())
|
|
196
|
+
finally:
|
|
197
|
+
os.close(devnull)
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__": # pragma: no cover
|
|
203
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""``ecdat about`` — credits, project links, and the animated globe.
|
|
2
|
+
|
|
3
|
+
The globe is decorative, so it is strictly opt-in: it only animates when the
|
|
4
|
+
output is an interactive terminal, motion is enabled
|
|
5
|
+
(:func:`ecdat.ui.motion.animations_enabled`), ``--no-anim`` is absent, and
|
|
6
|
+
``--spin`` is positive. When any of those is false, exactly one static frame
|
|
7
|
+
is printed — no cursor movement, no escape codes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
NAME = "about"
|
|
16
|
+
|
|
17
|
+
_DESCRIPTION = (
|
|
18
|
+
"ECDAT discovers cryptographic artefacts in source code and dependency "
|
|
19
|
+
"manifests, scores their post-quantum risk with Mosca's inequality, and "
|
|
20
|
+
"exports a CycloneDX 1.6 Cryptography Bill of Materials."
|
|
21
|
+
)
|
|
22
|
+
_LICENSE_LINE = "MIT \u00b7 https://github.com/profm0r14rty/ecdat"
|
|
23
|
+
_BUILT_WITH = "Built with Rich"
|
|
24
|
+
|
|
25
|
+
_FPS = 14.0
|
|
26
|
+
_GLOBE_MAX_WIDTH = 56
|
|
27
|
+
_GLOBE_MAX_HEIGHT = 22
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register(subparsers: "argparse._SubParsersAction") -> None:
|
|
31
|
+
"""Attach the ``about`` subcommand to *subparsers*."""
|
|
32
|
+
parser = subparsers.add_parser(
|
|
33
|
+
"about",
|
|
34
|
+
help="About ECDAT \u2014 credits and a spinning globe",
|
|
35
|
+
description=(
|
|
36
|
+
"Show the ECDAT banner, a short description, and (on an "
|
|
37
|
+
"interactive terminal) an animated 3D globe."
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--spin",
|
|
42
|
+
type=float,
|
|
43
|
+
default=3.0,
|
|
44
|
+
metavar="SECONDS",
|
|
45
|
+
help="Seconds to animate the globe (default: 3; 0 disables)",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--no-anim",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Never animate; print a single static frame",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run(args) -> int:
|
|
55
|
+
"""Print credits and optionally animate the globe; return 0."""
|
|
56
|
+
from rich.style import Style
|
|
57
|
+
from rich.text import Text
|
|
58
|
+
|
|
59
|
+
from ecdat import __version__
|
|
60
|
+
from ecdat.ui.banner import render_banner
|
|
61
|
+
from ecdat.ui.console import make_console
|
|
62
|
+
from ecdat.ui.motion import animations_enabled
|
|
63
|
+
from ecdat.ui.theme import PALETTE
|
|
64
|
+
|
|
65
|
+
no_color = True if getattr(args, "no_color", False) else None
|
|
66
|
+
console = make_console(no_color=no_color)
|
|
67
|
+
|
|
68
|
+
console.print(render_banner(version=__version__))
|
|
69
|
+
console.print(Text(_DESCRIPTION))
|
|
70
|
+
console.print(Text(_LICENSE_LINE, style=Style(color=PALETTE.muted)))
|
|
71
|
+
console.print(Text(_BUILT_WITH, style=Style(color=PALETTE.muted)))
|
|
72
|
+
console.print(Text())
|
|
73
|
+
|
|
74
|
+
width, height = _globe_size(console)
|
|
75
|
+
spin = max(0.0, float(getattr(args, "spin", 0.0)))
|
|
76
|
+
animate = (
|
|
77
|
+
not getattr(args, "no_anim", False)
|
|
78
|
+
and spin > 0.0
|
|
79
|
+
and console.is_terminal
|
|
80
|
+
and animations_enabled()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if animate:
|
|
84
|
+
_animate(console, width, height, spin)
|
|
85
|
+
else:
|
|
86
|
+
console.print(_frame_text(0.0, width, height))
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Helpers
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _globe_size(console) -> tuple[int, int]:
|
|
96
|
+
"""Return the globe frame size: terminal size capped at 56x22."""
|
|
97
|
+
size = console.size
|
|
98
|
+
return (
|
|
99
|
+
max(1, min(size.width, _GLOBE_MAX_WIDTH)),
|
|
100
|
+
max(1, min(size.height, _GLOBE_MAX_HEIGHT)),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _frame_text(elapsed: float, width: int, height: int):
|
|
105
|
+
"""Render one globe frame as gradient-coloured Rich Text."""
|
|
106
|
+
from ecdat.ui.art3d import render_globe
|
|
107
|
+
from ecdat.ui.art_text import frame_to_text
|
|
108
|
+
|
|
109
|
+
return frame_to_text(render_globe(elapsed, width, height))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _animate(console, width: int, height: int, seconds: float) -> None:
|
|
113
|
+
"""Animate the globe for *seconds* using real elapsed wall-clock time.
|
|
114
|
+
|
|
115
|
+
Uses :class:`rich.live.Live` with ``transient=False`` so the final frame
|
|
116
|
+
stays on screen. ``Ctrl-C`` propagates out (the Live context restores the
|
|
117
|
+
terminal, and the CLI maps it to exit code 130).
|
|
118
|
+
"""
|
|
119
|
+
from rich.live import Live
|
|
120
|
+
|
|
121
|
+
interval = 1.0 / _FPS
|
|
122
|
+
start = time.monotonic()
|
|
123
|
+
|
|
124
|
+
with Live(console=console, refresh_per_second=int(_FPS), transient=False) as live:
|
|
125
|
+
elapsed = 0.0
|
|
126
|
+
while elapsed < seconds:
|
|
127
|
+
elapsed = time.monotonic() - start
|
|
128
|
+
live.update(_frame_text(elapsed, width, height))
|
|
129
|
+
time.sleep(min(interval, max(0.0, seconds - elapsed)))
|
|
130
|
+
live.update(_frame_text(seconds, width, height))
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""``ecdat demo`` — scan the bundled sample project.
|
|
2
|
+
|
|
3
|
+
A zero-setup "try it now" command: copies the demo project shipped inside the
|
|
4
|
+
package to a temporary directory, scans it through the same
|
|
5
|
+
:func:`~ecdat.services.scanner.perform_scan` path as ``ecdat scan``, and
|
|
6
|
+
renders the usual report. ``--path`` instead materialises a persistent copy
|
|
7
|
+
under ``ECDAT_HOME/demo`` and prints its location.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
NAME = "demo"
|
|
17
|
+
|
|
18
|
+
_FORMATS = ("pretty", "json", "summary")
|
|
19
|
+
|
|
20
|
+
_PRETTY_NOTE = "Bundled sample code \u2014 intentionally insecure, never run it."
|
|
21
|
+
_LABEL = "demo project (bundled sample)"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register(subparsers: "argparse._SubParsersAction") -> None:
|
|
25
|
+
"""Attach the ``demo`` subcommand to *subparsers*."""
|
|
26
|
+
parser = subparsers.add_parser(
|
|
27
|
+
"demo",
|
|
28
|
+
help="Scan the bundled sample project (zero setup)",
|
|
29
|
+
description=(
|
|
30
|
+
"Scan ECDAT's bundled, deliberately-insecure sample project. "
|
|
31
|
+
"With --path, materialise a persistent copy under ECDAT_HOME/demo "
|
|
32
|
+
"instead of scanning."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"-f",
|
|
37
|
+
"--format",
|
|
38
|
+
choices=_FORMATS,
|
|
39
|
+
default="pretty",
|
|
40
|
+
help="Output format (default: pretty)",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--limit",
|
|
44
|
+
type=int,
|
|
45
|
+
default=15,
|
|
46
|
+
metavar="N",
|
|
47
|
+
help="Maximum findings shown in the pretty table (default: 15)",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--path",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="Copy the sample under ECDAT_HOME/demo and print its path",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run(args) -> int:
|
|
57
|
+
"""Run the demo scan (or materialise it); return the process exit code."""
|
|
58
|
+
from rich.style import Style
|
|
59
|
+
from rich.text import Text
|
|
60
|
+
|
|
61
|
+
from ecdat.services.paths import home_dir
|
|
62
|
+
from ecdat.ui.console import make_console
|
|
63
|
+
from ecdat.ui.theme import PALETTE
|
|
64
|
+
|
|
65
|
+
no_color = True if getattr(args, "no_color", False) else None
|
|
66
|
+
out_console = make_console(no_color=no_color)
|
|
67
|
+
err_console = make_console(stderr=True, no_color=no_color)
|
|
68
|
+
|
|
69
|
+
if getattr(args, "path", False):
|
|
70
|
+
from ecdat.services.demo import materialize_demo
|
|
71
|
+
|
|
72
|
+
destination = materialize_demo(home_dir() / "demo")
|
|
73
|
+
sys.stdout.write(str(destination) + "\n")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
from ecdat.services.demo import demo_project
|
|
77
|
+
from ecdat.services.scanner import classify_target, perform_scan
|
|
78
|
+
|
|
79
|
+
with demo_project() as path:
|
|
80
|
+
target = classify_target(str(path))
|
|
81
|
+
outcome = perform_scan(target, label=_LABEL)
|
|
82
|
+
result = outcome.result
|
|
83
|
+
vm = outcome.vm
|
|
84
|
+
|
|
85
|
+
output_format = getattr(args, "format", "pretty")
|
|
86
|
+
if output_format == "json":
|
|
87
|
+
_emit_json(result.model_dump(mode="json"))
|
|
88
|
+
elif output_format == "summary":
|
|
89
|
+
from ecdat_core.cbom_export import export_summary
|
|
90
|
+
|
|
91
|
+
_emit_json(export_summary(result))
|
|
92
|
+
else:
|
|
93
|
+
_render_pretty(out_console, vm, limit=getattr(args, "limit", 15))
|
|
94
|
+
|
|
95
|
+
err_console.print(Text(_PRETTY_NOTE, style=Style(color=PALETTE.muted, dim=True)))
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Output helpers
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _emit_json(payload: dict) -> None:
|
|
105
|
+
"""Write *payload* as pretty JSON to stdout — the ONLY thing on stdout."""
|
|
106
|
+
sys.stdout.write(json.dumps(payload, indent=2) + "\n")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _render_pretty(console, vm, *, limit: int) -> None:
|
|
110
|
+
"""Render the banner plus the standard scan report to stdout."""
|
|
111
|
+
from ecdat import __version__
|
|
112
|
+
from ecdat.ui.banner import render_banner
|
|
113
|
+
from ecdat.ui.render import scan_report
|
|
114
|
+
|
|
115
|
+
console.print(render_banner(version=__version__))
|
|
116
|
+
console.print(scan_report(vm, limit=limit))
|