ecdat 0.2.0__tar.gz → 0.3.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.
Files changed (170) hide show
  1. {ecdat-0.2.0/ecdat.egg-info → ecdat-0.3.0}/PKG-INFO +11 -5
  2. {ecdat-0.2.0 → ecdat-0.3.0}/PYPI_README.md +6 -0
  3. {ecdat-0.2.0 → ecdat-0.3.0}/README.md +7 -0
  4. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/__init__.py +2 -2
  5. ecdat-0.3.0/ecdat/cli/__init__.py +14 -0
  6. ecdat-0.3.0/ecdat/cli/commands/__init__.py +1 -0
  7. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/cli/commands/demo.py +10 -0
  8. ecdat-0.3.0/ecdat/cli/commands/explain.py +204 -0
  9. ecdat-0.3.0/ecdat/cli/commands/history_cmd.py +51 -0
  10. ecdat-0.3.0/ecdat/cli/commands/mosca.py +222 -0
  11. ecdat-0.3.0/ecdat/cli/commands/report.py +67 -0
  12. ecdat-0.3.0/ecdat/cli/commands/rules.py +175 -0
  13. ecdat-0.3.0/ecdat/cli/commands/tui_cmd.py +61 -0
  14. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/cli/commands/version_cmd.py +6 -0
  15. ecdat-0.2.0/ecdat/cli/__init__.py → ecdat-0.3.0/ecdat/cli/main.py +3 -3
  16. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/cli/parser.py +40 -1
  17. ecdat-0.3.0/ecdat/services/__init__.py +1 -0
  18. ecdat-0.3.0/ecdat/services/exporters.py +439 -0
  19. ecdat-0.3.0/ecdat/services/filtering.py +151 -0
  20. ecdat-0.3.0/ecdat/services/history.py +297 -0
  21. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/services/paths.py +46 -2
  22. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/services/scanner.py +134 -35
  23. ecdat-0.3.0/ecdat/services/settings.py +178 -0
  24. ecdat-0.3.0/ecdat/tests/__init__.py +1 -0
  25. ecdat-0.3.0/ecdat/tests/test_app_cli_mosca.py +243 -0
  26. ecdat-0.3.0/ecdat/tests/test_app_cli_report.py +174 -0
  27. ecdat-0.3.0/ecdat/tests/test_app_cli_rules.py +250 -0
  28. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_crashlog.py +2 -0
  29. ecdat-0.3.0/ecdat/tests/test_app_e2e_tui.py +160 -0
  30. ecdat-0.3.0/ecdat/tests/test_app_exporters.py +304 -0
  31. ecdat-0.3.0/ecdat/tests/test_app_filtering.py +222 -0
  32. ecdat-0.3.0/ecdat/tests/test_app_history.py +176 -0
  33. ecdat-0.3.0/ecdat/tests/test_app_installers.py +143 -0
  34. ecdat-0.3.0/ecdat/tests/test_app_motion.py +174 -0
  35. ecdat-0.3.0/ecdat/tests/test_app_offline.py +247 -0
  36. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_render.py +160 -2
  37. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_scanner.py +4 -2
  38. ecdat-0.3.0/ecdat/tests/test_app_settings.py +90 -0
  39. ecdat-0.3.0/ecdat/tests/test_app_tui_art_view.py +82 -0
  40. ecdat-0.3.0/ecdat/tests/test_app_tui_export_history.py +421 -0
  41. ecdat-0.3.0/ecdat/tests/test_app_tui_findings.py +545 -0
  42. ecdat-0.3.0/ecdat/tests/test_app_tui_foundation.py +256 -0
  43. ecdat-0.3.0/ecdat/tests/test_app_tui_home.py +209 -0
  44. ecdat-0.3.0/ecdat/tests/test_app_tui_polish.py +293 -0
  45. ecdat-0.3.0/ecdat/tests/test_app_tui_results.py +270 -0
  46. ecdat-0.3.0/ecdat/tests/test_app_tui_safe_header.py +126 -0
  47. ecdat-0.3.0/ecdat/tests/test_app_tui_scan.py +264 -0
  48. ecdat-0.3.0/ecdat/tests/test_app_tui_splash.py +243 -0
  49. ecdat-0.3.0/ecdat/tests/test_app_tui_startup.py +153 -0
  50. ecdat-0.3.0/ecdat/tests/test_app_tui_tabs.py +448 -0
  51. ecdat-0.3.0/ecdat/tests/test_app_untrusted.py +612 -0
  52. ecdat-0.3.0/ecdat/tests/tui_helpers.py +61 -0
  53. ecdat-0.3.0/ecdat/tests/vm_factory.py +285 -0
  54. ecdat-0.3.0/ecdat/tui/__init__.py +48 -0
  55. ecdat-0.3.0/ecdat/tui/app.py +453 -0
  56. ecdat-0.3.0/ecdat/tui/ecdat.tcss +587 -0
  57. ecdat-0.3.0/ecdat/tui/screens/__init__.py +1 -0
  58. ecdat-0.3.0/ecdat/tui/screens/export.py +221 -0
  59. ecdat-0.3.0/ecdat/tui/screens/help.py +119 -0
  60. ecdat-0.3.0/ecdat/tui/screens/history.py +249 -0
  61. ecdat-0.3.0/ecdat/tui/screens/home.py +292 -0
  62. ecdat-0.3.0/ecdat/tui/screens/pick_dir.py +77 -0
  63. ecdat-0.3.0/ecdat/tui/screens/results.py +301 -0
  64. ecdat-0.3.0/ecdat/tui/screens/scan.py +408 -0
  65. ecdat-0.3.0/ecdat/tui/screens/splash.py +116 -0
  66. ecdat-0.3.0/ecdat/tui/theme.py +97 -0
  67. ecdat-0.3.0/ecdat/tui/widgets/__init__.py +1 -0
  68. ecdat-0.3.0/ecdat/tui/widgets/art_view.py +150 -0
  69. ecdat-0.3.0/ecdat/tui/widgets/banner.py +58 -0
  70. ecdat-0.3.0/ecdat/tui/widgets/file_tree.py +299 -0
  71. ecdat-0.3.0/ecdat/tui/widgets/finding_detail.py +266 -0
  72. ecdat-0.3.0/ecdat/tui/widgets/findings_table.py +433 -0
  73. ecdat-0.3.0/ecdat/tui/widgets/header.py +60 -0
  74. ecdat-0.3.0/ecdat/tui/widgets/mosca_timeline.py +186 -0
  75. ecdat-0.3.0/ecdat/tui/widgets/priority_list.py +136 -0
  76. ecdat-0.3.0/ecdat/tui/widgets/recommendations.py +222 -0
  77. ecdat-0.3.0/ecdat/tui/widgets/risk_chart.py +197 -0
  78. ecdat-0.3.0/ecdat/tui/widgets/stat_card.py +101 -0
  79. ecdat-0.3.0/ecdat/ui/__init__.py +16 -0
  80. ecdat-0.3.0/ecdat/ui/motion.py +223 -0
  81. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/ui/render.py +214 -6
  82. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/ui/theme.py +131 -9
  83. {ecdat-0.2.0 → ecdat-0.3.0/ecdat.egg-info}/PKG-INFO +11 -5
  84. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat.egg-info/SOURCES.txt +65 -0
  85. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat.egg-info/requires.txt +2 -0
  86. ecdat-0.3.0/ecdat_core/cli.py +271 -0
  87. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/ingestion.py +116 -17
  88. ecdat-0.3.0/ecdat_core/progress.py +90 -0
  89. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/risk_engine.py +17 -3
  90. ecdat-0.3.0/ecdat_core/tests/test_ingestion_realworld.py +185 -0
  91. ecdat-0.3.0/ecdat_core/tests/test_progress.py +111 -0
  92. {ecdat-0.2.0 → ecdat-0.3.0}/pyproject.toml +7 -5
  93. ecdat-0.2.0/ecdat/cli/commands/__init__.py +0 -0
  94. ecdat-0.2.0/ecdat/services/__init__.py +0 -1
  95. ecdat-0.2.0/ecdat/tests/__init__.py +0 -0
  96. ecdat-0.2.0/ecdat/ui/__init__.py +0 -1
  97. ecdat-0.2.0/ecdat/ui/motion.py +0 -64
  98. ecdat-0.2.0/ecdat_core/cli.py +0 -202
  99. {ecdat-0.2.0 → ecdat-0.3.0}/CHANGELOG.md +0 -0
  100. {ecdat-0.2.0 → ecdat-0.3.0}/LICENSE +0 -0
  101. {ecdat-0.2.0 → ecdat-0.3.0}/MANIFEST.in +0 -0
  102. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/__main__.py +0 -0
  103. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/cli/commands/about.py +0 -0
  104. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/cli/commands/doctor.py +0 -0
  105. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/cli/commands/help_cmd.py +0 -0
  106. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/cli/commands/scan.py +0 -0
  107. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/demo_project/auth/login.py +0 -0
  108. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/demo_project/certs/cert_verify.go +0 -0
  109. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/demo_project/keyexchange/channel.go +0 -0
  110. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/demo_project/legacy/LegacyCrypto.java +0 -0
  111. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/demo_project/payments/payment.py +0 -0
  112. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/demo_project/quantum/pqc_utils.py +0 -0
  113. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/demo_project/quantum/slh_signer.py +0 -0
  114. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/demo_project/tokens/signing.js +0 -0
  115. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/py.typed +0 -0
  116. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/services/crashlog.py +0 -0
  117. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/services/demo.py +0 -0
  118. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/services/viewmodel.py +0 -0
  119. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/conftest.py +0 -0
  120. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_art.py +0 -0
  121. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_cli_meta.py +0 -0
  122. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_cli_scan.py +0 -0
  123. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_demo.py +0 -0
  124. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_e2e_cli.py +0 -0
  125. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_packaging.py +0 -0
  126. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_skeleton.py +0 -0
  127. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_ui.py +0 -0
  128. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/tests/test_app_viewmodel.py +0 -0
  129. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/ui/art3d.py +0 -0
  130. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/ui/art_static.py +0 -0
  131. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/ui/art_text.py +0 -0
  132. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/ui/banner.py +0 -0
  133. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat/ui/console.py +0 -0
  134. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat.egg-info/dependency_links.txt +0 -0
  135. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat.egg-info/entry_points.txt +0 -0
  136. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat.egg-info/top_level.txt +0 -0
  137. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/__init__.py +0 -0
  138. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/cbom_export.py +0 -0
  139. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/detector.py +0 -0
  140. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/models.py +0 -0
  141. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/recommender.py +0 -0
  142. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/signature_loader.py +0 -0
  143. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/signatures.json +0 -0
  144. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/cyclonedx/bom-1.6.schema.json +0 -0
  145. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/cyclonedx/jsf-0.82.schema.json +0 -0
  146. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/cyclonedx/spdx.schema.json +0 -0
  147. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/demo_repo/auth/login.py +0 -0
  148. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/demo_repo/legacy/cipher.java +0 -0
  149. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/demo_repo/modern/crypto.py +0 -0
  150. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/demo_repo/requirements.txt +0 -0
  151. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/demo_repo/utils/hasher.py +0 -0
  152. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/showcase_repo/auth/login.py +0 -0
  153. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/showcase_repo/certs/cert_verify.go +0 -0
  154. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/showcase_repo/keyexchange/channel.go +0 -0
  155. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/showcase_repo/legacy/LegacyCrypto.java +0 -0
  156. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/showcase_repo/payments/payment.py +0 -0
  157. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/showcase_repo/quantum/pqc_utils.py +0 -0
  158. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/showcase_repo/quantum/slh_signer.py +0 -0
  159. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/fixtures/showcase_repo/tokens/signing.js +0 -0
  160. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_cbom_export.py +0 -0
  161. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_cbom_schema.py +0 -0
  162. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_detector.py +0 -0
  163. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_end_to_end.py +0 -0
  164. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_ingestion.py +0 -0
  165. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_models.py +0 -0
  166. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_recommender.py +0 -0
  167. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_risk_engine.py +0 -0
  168. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_run_scan_sandbox.py +0 -0
  169. {ecdat-0.2.0 → ecdat-0.3.0}/ecdat_core/tests/test_signature_loader.py +0 -0
  170. {ecdat-0.2.0 → ecdat-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ecdat
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Cryptographic discovery and post-quantum readiness scanner — CLI with a gradient ASCII banner and an animated 3D globe (CBOM, CycloneDX 1.6)
5
5
  Author: ECDAT Team
6
6
  License-Expression: MIT
@@ -8,36 +8,42 @@ Project-URL: Homepage, https://github.com/profm0r14rty/ecdat
8
8
  Project-URL: Repository, https://github.com/profm0r14rty/ecdat
9
9
  Project-URL: Issues, https://github.com/profm0r14rty/ecdat/issues
10
10
  Project-URL: Changelog, https://github.com/profm0r14rty/ecdat/blob/main/CHANGELOG.md
11
- Keywords: cbom,cli,cryptography,cyclonedx,post-quantum,pqc,quantum,sbom,security
11
+ Keywords: cbom,cli,cryptography,cyclonedx,post-quantum,pqc,quantum,sbom,security,tui
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: Information Technology
16
16
  Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
18
  Classifier: Programming Language :: Python :: 3.12
21
19
  Classifier: Programming Language :: Python :: 3.13
22
20
  Classifier: Programming Language :: Python :: 3.14
23
21
  Classifier: Topic :: Security
24
22
  Classifier: Topic :: Security :: Cryptography
25
23
  Classifier: Topic :: Software Development :: Quality Assurance
26
- Requires-Python: >=3.10
24
+ Requires-Python: >=3.12
27
25
  Description-Content-Type: text/markdown
28
26
  License-File: LICENSE
29
27
  Requires-Dist: pydantic>=2
30
28
  Requires-Dist: rich>=13.7
29
+ Requires-Dist: textual<9,>=8.2
31
30
  Provides-Extra: dev
32
31
  Requires-Dist: pytest; extra == "dev"
33
32
  Requires-Dist: pytest-cov; extra == "dev"
34
33
  Requires-Dist: jsonschema; extra == "dev"
35
34
  Requires-Dist: build; extra == "dev"
36
35
  Requires-Dist: twine; extra == "dev"
36
+ Requires-Dist: pytest-asyncio; extra == "dev"
37
37
  Dynamic: license-file
38
38
 
39
39
  # ecdat
40
40
 
41
+ [![PyPI](https://img.shields.io/pypi/v/ecdat?color=3ddc97&label=PyPI)](https://pypi.org/project/ecdat/)
42
+ [![Python](https://img.shields.io/pypi/pyversions/ecdat?color=3ddc97)](https://pypi.org/project/ecdat/)
43
+ [![Downloads](https://img.shields.io/pypi/dm/ecdat?color=3ddc97&label=downloads)](https://pypi.org/project/ecdat/)
44
+ [![License: MIT](https://img.shields.io/github/license/profm0r14rty/ECDAT?color=3ddc97)](https://github.com/profm0r14rty/ECDAT/blob/main/LICENSE)
45
+ [![SIH 2026](https://img.shields.io/badge/SIH%202026%20Internals-1st%20Place%20🏆-gold)](https://github.com/profm0r14rty/ECDAT)
46
+
41
47
  **ECDAT** is a [CycloneDX](https://cyclonedx.org/) **Cryptography Bill of
42
48
  Materials (CBOM) scanner for post-quantum readiness**. One command finds the
43
49
  cryptographic artefacts in a codebase — RSA, ECC, DH, DSA, MD5, SHA-1, DES,
@@ -1,5 +1,11 @@
1
1
  # ecdat
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/ecdat?color=3ddc97&label=PyPI)](https://pypi.org/project/ecdat/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/ecdat?color=3ddc97)](https://pypi.org/project/ecdat/)
5
+ [![Downloads](https://img.shields.io/pypi/dm/ecdat?color=3ddc97&label=downloads)](https://pypi.org/project/ecdat/)
6
+ [![License: MIT](https://img.shields.io/github/license/profm0r14rty/ECDAT?color=3ddc97)](https://github.com/profm0r14rty/ECDAT/blob/main/LICENSE)
7
+ [![SIH 2026](https://img.shields.io/badge/SIH%202026%20Internals-1st%20Place%20🏆-gold)](https://github.com/profm0r14rty/ECDAT)
8
+
3
9
  **ECDAT** is a [CycloneDX](https://cyclonedx.org/) **Cryptography Bill of
4
10
  Materials (CBOM) scanner for post-quantum readiness**. One command finds the
5
11
  cryptographic artefacts in a codebase — RSA, ECC, DH, DSA, MD5, SHA-1, DES,
@@ -1,5 +1,12 @@
1
1
  # ECDAT - Enterprise Cryptographic Discovery & Analysis Tool
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/ecdat?color=3ddc97&label=PyPI)](https://pypi.org/project/ecdat/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/ecdat?color=3ddc97)](https://pypi.org/project/ecdat/)
5
+ [![Downloads](https://img.shields.io/pypi/dm/ecdat?color=3ddc97&label=downloads)](https://pypi.org/project/ecdat/)
6
+ [![License: MIT](https://img.shields.io/github/license/profm0r14rty/ECDAT?color=3ddc97)](https://github.com/profm0r14rty/ECDAT/blob/main/LICENSE)
7
+ [![CI](https://github.com/profm0r14rty/ECDAT/actions/workflows/ecdat-ci.yml/badge.svg)](https://github.com/profm0r14rty/ECDAT/actions/workflows/ecdat-ci.yml)
8
+ [![SIH 2026](https://img.shields.io/badge/SIH%202026%20Internals-1st%20Place%20🏆-gold)](https://github.com/profm0r14rty/ECDAT)
9
+
3
10
  ECDAT is a Cryptography Bill of Materials (CBOM) scanner for post-quantum readiness assessment. It discovers cryptographic artefacts in source code, assesses their quantum-computing risk using Mosca's algorithm, and recommends NIST post-quantum replacements, outputting a CycloneDX 1.6 CBOM report.
4
11
 
5
12
  ## Install the CLI
@@ -1,4 +1,4 @@
1
- """ECDAT — Enterprise Cryptographic Discovery & Analysis Tool."""
1
+ """ECDAT — Enterprise Cryptographic Discovery & Analysis Tool (app layer)."""
2
2
 
3
3
  import importlib.metadata
4
4
 
@@ -7,4 +7,4 @@ DIST_NAME = "ecdat"
7
7
  try:
8
8
  __version__ = importlib.metadata.version(DIST_NAME)
9
9
  except importlib.metadata.PackageNotFoundError:
10
- __version__ = "0+unknown"
10
+ __version__ = "0+unknown"
@@ -0,0 +1,14 @@
1
+ """ECDAT command-line interface.
2
+
3
+ The :func:`main` entry point (wired as the ``ecdat`` console script and used by
4
+ ``python -m ecdat``) lives in :mod:`ecdat.cli.main`; it is re-exported here so
5
+ both ``from ecdat.cli import main`` and ``from ecdat.cli.main import main``
6
+ resolve to the same callable. Importing this package stays cheap — Rich, the
7
+ scanner engine, and Textual are all imported lazily by the command modules.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from ecdat.cli.main import main
13
+
14
+ __all__ = ["main"]
@@ -0,0 +1 @@
1
+ """ECDAT CLI subcommands."""
@@ -51,6 +51,11 @@ def register(subparsers: "argparse._SubParsersAction") -> None:
51
51
  action="store_true",
52
52
  help="Copy the sample under ECDAT_HOME/demo and print its path",
53
53
  )
54
+ parser.add_argument(
55
+ "--tui",
56
+ action="store_true",
57
+ help="Open the animated TUI straight into the demo scan",
58
+ )
54
59
 
55
60
 
56
61
  def run(args) -> int:
@@ -66,6 +71,11 @@ def run(args) -> int:
66
71
  out_console = make_console(no_color=no_color)
67
72
  err_console = make_console(stderr=True, no_color=no_color)
68
73
 
74
+ if getattr(args, "tui", False):
75
+ from ecdat.tui import run_tui
76
+
77
+ return run_tui(auto_start=True, demo=True)
78
+
69
79
  if getattr(args, "path", False):
70
80
  from ecdat.services.demo import materialize_demo
71
81
 
@@ -0,0 +1,204 @@
1
+ """``ecdat explain`` — explain an algorithm in plain English.
2
+
3
+ Looks up a signature entry by name (or family) and renders a panel describing
4
+ what it is, its quantum status, whether it is classically broken, how ECDAT
5
+ scores it with Mosca's inequality, the recommended NIST replacement, and the
6
+ languages ECDAT detects it in.
7
+
8
+ The knowledge base is read through the public
9
+ :mod:`ecdat_core.signature_loader` API. Every dynamic value is wrapped in
10
+ :class:`rich.text.Text` so knowledge-base text is never parsed as markup.
11
+
12
+ Note:
13
+ :class:`~ecdat_core.signature_loader.SignatureEntry` has no ``aliases``
14
+ field, so the lookup covers ``name`` and ``family`` — the fields the real
15
+ model actually exposes.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import difflib
22
+ from typing import List
23
+
24
+ from rich.panel import Panel
25
+ from rich.text import Text
26
+
27
+ from ecdat.ui.theme import PALETTE, make_console
28
+ from ecdat_core.signature_loader import SignatureEntry, get_all_signatures
29
+
30
+ NAME = "explain"
31
+
32
+
33
+ def register(subparsers: "argparse._SubParsersAction") -> None:
34
+ """Attach the ``explain`` subcommand to *subparsers*."""
35
+ parser = subparsers.add_parser(
36
+ "explain",
37
+ help="Explain an algorithm in plain English",
38
+ description=(
39
+ "Explain a signature by name or family: what it is, its quantum "
40
+ "status, how Mosca's inequality scores it, and what replaces it."
41
+ ),
42
+ )
43
+ parser.add_argument(
44
+ "name",
45
+ help="Signature name or family to explain (case-insensitive)",
46
+ )
47
+
48
+
49
+ # One-paragraph, static explanation of the scoring model. Kept as a module
50
+ # constant so the wording is consistent across every explained algorithm.
51
+ _MOSCA_PARAGRAPH = (
52
+ "ECDAT scores this with Mosca's inequality, X + Y > Z: X is the time to "
53
+ "migrate away from the algorithm, Y is how long the protected data must "
54
+ "stay secret, and Z is the estimated arrival of a cryptographically "
55
+ "relevant quantum computer. When X + Y exceeds Z the artefact is already "
56
+ "exposed, and the urgency ratio (X + Y) / Z classifies it from low through "
57
+ "critical."
58
+ )
59
+
60
+
61
+ def _find_matches(name: str) -> List[SignatureEntry]:
62
+ """Return entries whose name or family matches *name* case-insensitively."""
63
+ needle = name.strip().lower()
64
+ return [
65
+ entry
66
+ for entry in get_all_signatures()
67
+ if entry.name.lower() == needle or entry.family.lower() == needle
68
+ ]
69
+
70
+
71
+ def _quantum_status(entry: SignatureEntry) -> str:
72
+ """Return the plain-English quantum status for *entry*.
73
+
74
+ - Symmetric families with a ``min_quantum_safe_key_bits`` threshold are
75
+ described as weakened by Grover's algorithm up to that key size.
76
+ - Otherwise quantum-vulnerable entries are broken by Shor's algorithm.
77
+ - Everything else is not affected.
78
+ """
79
+ if entry.min_quantum_safe_key_bits is not None:
80
+ return (
81
+ "weakened by Grover's algorithm \u2014 needs at least "
82
+ f"{entry.min_quantum_safe_key_bits}-bit keys"
83
+ )
84
+ if entry.quantum_vulnerable:
85
+ return "broken by Shor's algorithm"
86
+ return "not affected by quantum algorithms (quantum-safe)"
87
+
88
+
89
+ def _build_body(entry: SignatureEntry) -> Text:
90
+ """Build the panel body as a :class:`~rich.text.Text` (markup-safe).
91
+
92
+ Args:
93
+ entry: The matched signature entry.
94
+
95
+ Returns:
96
+ A styled :class:`~rich.text.Text` ready to place inside a panel.
97
+ """
98
+ body = Text()
99
+
100
+ def section(title: str) -> None:
101
+ body.append(f"{title}\n", style=f"bold {PALETTE.accent}")
102
+
103
+ def field(label: str, value: str) -> None:
104
+ body.append(f"{label}: ", style=f"bold {PALETTE.text}")
105
+ body.append(f"{value}\n")
106
+
107
+ # What it is.
108
+ section("What it is")
109
+ field("Family", entry.family)
110
+ body.append("\n")
111
+
112
+ # Quantum status.
113
+ section("Quantum status")
114
+ field("Status", _quantum_status(entry))
115
+ if entry.classically_broken:
116
+ body.append("Warning: ", style=f"bold {PALETTE.critical}")
117
+ body.append(
118
+ "already broken today by classical attacks, independent of "
119
+ "quantum computing \u2014 do not treat it as quantum-safe.\n",
120
+ style=PALETTE.critical,
121
+ )
122
+ body.append("\n")
123
+
124
+ # How ECDAT scores it.
125
+ section("How ECDAT scores it")
126
+ body.append(_MOSCA_PARAGRAPH + "\n")
127
+ body.append("\n")
128
+
129
+ # Recommended replacement.
130
+ section("Recommended replacement")
131
+ field("Algorithm", entry.pqc_recommendation.algorithm)
132
+ field("FIPS reference", entry.pqc_recommendation.fips_reference)
133
+ body.append("\n")
134
+
135
+ # Detection coverage.
136
+ section("Detected in languages")
137
+ body.append(", ".join(sorted(entry.patterns)) + "\n")
138
+
139
+ return body
140
+
141
+
142
+ def _render_panel(entry: SignatureEntry) -> Panel:
143
+ """Render the explanation panel for a single matched entry."""
144
+ return Panel(
145
+ _build_body(entry),
146
+ title=Text(f"ecdat explain \u00b7 {entry.name}"),
147
+ title_align="left",
148
+ border_style=PALETTE.border,
149
+ padding=(1, 2),
150
+ )
151
+
152
+
153
+ def run(args: object) -> int:
154
+ """Execute the ``explain`` subcommand.
155
+
156
+ Args:
157
+ args: Parsed arguments with attribute ``name``.
158
+
159
+ Returns:
160
+ Exit code: 0 when exactly one entry matched; 2 when several or none
161
+ matched (several → the candidates are listed; none → close matches are
162
+ suggested).
163
+ """
164
+ name: str = getattr(args, "name", "")
165
+ out = make_console()
166
+
167
+ matches = _find_matches(name)
168
+
169
+ if len(matches) == 1:
170
+ out.print(_render_panel(matches[0]))
171
+ return 0
172
+
173
+ if len(matches) > 1:
174
+ out.print(
175
+ Text(
176
+ f"{len(matches)} signatures match {name!r}; "
177
+ f"be more specific:"
178
+ )
179
+ )
180
+ for entry in sorted(matches, key=lambda e: e.name):
181
+ out.print(Text(f" {entry.name} \u2014 {entry.family}"))
182
+ return 2
183
+
184
+ # No match — offer close suggestions from the real knowledge base.
185
+ names = [entry.name for entry in get_all_signatures()]
186
+ suggestions = difflib.get_close_matches(
187
+ name.strip().lower(), [n.lower() for n in names], n=3
188
+ )
189
+ # Map lowercased suggestions back to their canonical names.
190
+ by_lower = {n.lower(): n for n in names}
191
+ canonical = [by_lower[s] for s in suggestions]
192
+
193
+ if canonical:
194
+ out.print(Text(f"No signature named {name!r}. Did you mean:"))
195
+ for suggestion in canonical:
196
+ out.print(Text(f" {suggestion}"))
197
+ else:
198
+ out.print(
199
+ Text(
200
+ f"No signature named {name!r} "
201
+ f"(0 of {len(names)} signatures)."
202
+ )
203
+ )
204
+ return 2
@@ -0,0 +1,51 @@
1
+ """``ecdat history`` — list saved scan records."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from ecdat.services.history import load_history
9
+
10
+ NAME = "history"
11
+
12
+
13
+ def register(subparsers: "argparse._SubParsersAction") -> None:
14
+ """Attach the ``history`` subcommand to *subparsers*."""
15
+ subparsers.add_parser(
16
+ "history",
17
+ help="Show scan history",
18
+ description="List the scans saved to the local ECDAT history store.",
19
+ )
20
+
21
+
22
+ def run(args: object) -> int:
23
+ """Execute the ``history`` subcommand.
24
+
25
+ Args:
26
+ args: Parsed arguments (unused for history — no options).
27
+
28
+ Returns:
29
+ Exit code (always 0).
30
+ """
31
+ records = load_history()
32
+
33
+ if not records:
34
+ print("No scan history found.", file=sys.stderr)
35
+ return 0
36
+
37
+ # Header
38
+ header = f"{'#':<4} {'Scan ID':<38} {'Target':<36} {'Detections':>10} {'Scanned at'}"
39
+ print(header)
40
+ print("-" * len(header))
41
+
42
+ for i, rec in enumerate(records, start=1):
43
+ # Truncate long fields.
44
+ scan_id = rec.scan_id[:36]
45
+ target = rec.target[:34] + ".." if len(rec.target) > 36 else rec.target
46
+ detections = str(len(rec.detections))
47
+ scanned = rec.scanned_at[:19] # ISO datetime (no TZ offset detail)
48
+
49
+ print(f"{i:<4} {scan_id:<38} {target:<36} {detections:>10} {scanned}")
50
+
51
+ return 0
@@ -0,0 +1,222 @@
1
+ """``ecdat mosca`` — Mosca's-inequality calculator with a visual timeline.
2
+
3
+ Given X (data lifetime), Y (migration time) and Z (quantum arrival horizon),
4
+ this computes the urgency ratio ``(X + Y) / Z``, classifies it with the engine's
5
+ own :func:`ecdat_core.risk_engine.classify_urgency` (the exact thresholds the
6
+ scanner uses — no duplicated constants), and draws the timeline from
7
+ :func:`ecdat.ui.render.mosca_timeline`.
8
+
9
+ Variable naming follows the Phase 77 spec: ``-x``/``--shelf-life`` is the data
10
+ lifetime, ``-y``/``--migration`` is the migration time, ``-z``/``--threat`` is
11
+ the quantum arrival horizon.
12
+
13
+ Exit codes:
14
+ - 0 — computed successfully.
15
+ - 2 — usage/validation error (missing/invalid arguments).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import sys
22
+ from typing import Optional
23
+
24
+ from rich.panel import Panel
25
+ from rich.prompt import Prompt
26
+ from rich.text import Text
27
+
28
+ from ecdat.ui.render import mosca_timeline
29
+ from ecdat.ui.theme import PALETTE, RISK_COLORS, RISK_LABELS, make_console
30
+ from ecdat_core.risk_engine import classify_urgency
31
+
32
+ NAME = "mosca"
33
+
34
+
35
+ def register(subparsers: "argparse._SubParsersAction") -> None:
36
+ """Attach the ``mosca`` subcommand to *subparsers*."""
37
+ parser = subparsers.add_parser(
38
+ "mosca",
39
+ help="Mosca's-inequality calculator with a visual timeline",
40
+ description=(
41
+ "Compute the Mosca urgency ratio (X + Y) / Z from a data "
42
+ "lifetime (X), migration time (Y) and quantum horizon (Z) and "
43
+ "draw the exposure timeline."
44
+ ),
45
+ )
46
+ parser.add_argument(
47
+ "-x",
48
+ "--shelf-life",
49
+ type=float,
50
+ default=None,
51
+ dest="x",
52
+ help="X — data lifetime in years (data must stay secret)",
53
+ )
54
+ parser.add_argument(
55
+ "-y",
56
+ "--migration",
57
+ type=float,
58
+ default=None,
59
+ dest="y",
60
+ help="Y — migration time in years",
61
+ )
62
+ parser.add_argument(
63
+ "-z",
64
+ "--threat",
65
+ type=float,
66
+ default=None,
67
+ dest="z",
68
+ help="Z — quantum threat horizon in years",
69
+ )
70
+
71
+
72
+ def _is_interactive() -> bool:
73
+ """Return whether we may prompt the user for missing values.
74
+
75
+ Interactive means both stdin and stdout are TTYs — prompting into a pipe or
76
+ a redirected stream would either hang or corrupt the payload.
77
+ """
78
+ try:
79
+ return bool(sys.stdin.isatty() and sys.stdout.isatty())
80
+ except (AttributeError, ValueError):
81
+ return False
82
+
83
+
84
+ def _fmt(value: float) -> str:
85
+ """Format a year value without a trailing ``.0`` for whole numbers."""
86
+ return str(int(value)) if float(value).is_integer() else f"{value:g}"
87
+
88
+
89
+ def _risk_chip(level: str) -> Text:
90
+ """Return a coloured uppercase risk chip for *level*."""
91
+ color = RISK_COLORS.get(level, PALETTE.text)
92
+ label = RISK_LABELS.get(level, level.upper())
93
+ return Text(label, style=f"bold {color}")
94
+
95
+
96
+ def _resolve(
97
+ raw: Optional[float],
98
+ *,
99
+ label: str,
100
+ flag: str,
101
+ out,
102
+ ) -> Optional[float]:
103
+ """Resolve a possibly-missing value, prompting only when interactive.
104
+
105
+ Args:
106
+ raw: The parsed value, or ``None`` when the flag was omitted.
107
+ label: Human-readable name used in the prompt ("data lifetime").
108
+ flag: The CLI flag spelling used in the prompt (``-x/--shelf-life``).
109
+ out: The console used for the interactive prompt.
110
+
111
+ Returns:
112
+ The resolved float value, or ``None`` when a value is required but the
113
+ session is non-interactive (the caller then returns a usage error).
114
+ """
115
+ if raw is not None:
116
+ return raw
117
+ if not _is_interactive():
118
+ return None
119
+ entered = Prompt.ask(
120
+ Text(f"{label} ({flag})", style=PALETTE.accent),
121
+ console=out,
122
+ )
123
+ try:
124
+ return float(entered)
125
+ except (TypeError, ValueError):
126
+ return None
127
+
128
+
129
+ def run(args: object) -> int:
130
+ """Execute the ``mosca`` subcommand.
131
+
132
+ Args:
133
+ args: Parsed arguments with attributes ``x`` (data lifetime),
134
+ ``y`` (migration time) and ``z`` (threat horizon), any of which may
135
+ be ``None``.
136
+
137
+ Returns:
138
+ Exit code (0 on success, 2 on usage/validation error).
139
+ """
140
+ out = make_console()
141
+
142
+ raw_x: Optional[float] = getattr(args, "x", None)
143
+ raw_y: Optional[float] = getattr(args, "y", None)
144
+ raw_z: Optional[float] = getattr(args, "z", None)
145
+
146
+ x = _resolve(raw_x, label="Data lifetime", flag="-x/--shelf-life", out=out)
147
+ if x is None:
148
+ print(
149
+ "Error: data lifetime (-x/--shelf-life) is required and must be a "
150
+ "number.",
151
+ file=sys.stderr,
152
+ )
153
+ return 2
154
+
155
+ y = _resolve(raw_y, label="Migration time", flag="-y/--migration", out=out)
156
+ if y is None:
157
+ print(
158
+ "Error: migration time (-y/--migration) is required and must be a "
159
+ "number.",
160
+ file=sys.stderr,
161
+ )
162
+ return 2
163
+
164
+ z = _resolve(raw_z, label="Quantum arrival", flag="-z/--threat", out=out)
165
+ if z is None:
166
+ print(
167
+ "Error: quantum arrival (-z/--threat) is required and must be a "
168
+ "number.",
169
+ file=sys.stderr,
170
+ )
171
+ return 2
172
+
173
+ if x < 0 or y < 0:
174
+ print(
175
+ "Error: data lifetime (X) and migration time (Y) must be >= 0.",
176
+ file=sys.stderr,
177
+ )
178
+ return 2
179
+ if z <= 0:
180
+ print("Error: quantum arrival (Z) must be > 0.", file=sys.stderr)
181
+ return 2
182
+
183
+ total = x + y
184
+ urgency = total / z
185
+ level = classify_urgency(urgency)
186
+ violation = total > z
187
+
188
+ # --- headline numbers + risk chip -------------------------------------
189
+ line = Text()
190
+ line.append(f"X + Y = {_fmt(total)} vs Z = {_fmt(z)}")
191
+ line.append(" urgency ratio ")
192
+ line.append(_fmt(round(urgency, 2)))
193
+ line.append(" ")
194
+ line.append(_risk_chip(level))
195
+
196
+ # --- plain-English verdict -------------------------------------------
197
+ if violation:
198
+ verdict = Text(
199
+ f"You would be exposed for {_fmt(total - z)} years.",
200
+ style=f"bold {PALETTE.critical}",
201
+ )
202
+ else:
203
+ verdict = Text(
204
+ f"You have a {_fmt(z - total)}-year safety margin.",
205
+ style=PALETTE.safe,
206
+ )
207
+
208
+ out.print(
209
+ Panel(
210
+ Text.assemble(
211
+ line,
212
+ "\n\n",
213
+ verdict,
214
+ ),
215
+ title="ecdat mosca",
216
+ title_align="left",
217
+ border_style=PALETTE.border,
218
+ padding=(1, 2),
219
+ )
220
+ )
221
+ out.print(mosca_timeline(x, y, z))
222
+ return 0
@@ -0,0 +1,67 @@
1
+ """``ecdat report`` — show a Markdown or HTML report for a scan."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from ecdat.services.exporters import export_html, export_markdown
9
+ from ecdat.services.history import get_by_id, get_latest
10
+
11
+ NAME = "report"
12
+
13
+
14
+ def register(subparsers: "argparse._SubParsersAction") -> None:
15
+ """Attach the ``report`` subcommand to *subparsers*."""
16
+ parser = subparsers.add_parser(
17
+ "report",
18
+ help="Show a report for a scan",
19
+ description="Re-render a saved scan as Markdown or HTML.",
20
+ )
21
+ parser.add_argument(
22
+ "scan_ref",
23
+ help="'latest' or a scan ID",
24
+ )
25
+ parser.add_argument(
26
+ "-f",
27
+ "--format",
28
+ choices=["markdown", "html"],
29
+ default="markdown",
30
+ dest="format",
31
+ help="Output format (default: markdown)",
32
+ )
33
+
34
+
35
+ def run(args: object) -> int:
36
+ """Execute the ``report`` subcommand.
37
+
38
+ Args:
39
+ args: Parsed arguments with attributes ``scan_ref`` and ``format``.
40
+
41
+ Returns:
42
+ Exit code (0 on success, 1 if scan not found, 2 on usage error).
43
+ """
44
+ scan_ref: str = args.scan_ref # type: ignore[union-attr]
45
+ fmt: str = args.format # type: ignore[union-attr]
46
+
47
+ if scan_ref == "latest":
48
+ result = get_latest()
49
+ else:
50
+ result = get_by_id(scan_ref)
51
+
52
+ if result is None:
53
+ if scan_ref == "latest":
54
+ print("Error: no scan history found", file=sys.stderr)
55
+ else:
56
+ print(f"Error: scan {scan_ref!r} not found in history", file=sys.stderr)
57
+ return 1
58
+
59
+ if fmt == "markdown":
60
+ sys.stdout.write(export_markdown(result))
61
+ elif fmt == "html":
62
+ sys.stdout.write(export_html(result))
63
+ else:
64
+ print(f"Error: unknown format {fmt!r}", file=sys.stderr)
65
+ return 2
66
+
67
+ return 0