veritensor 1.6.3__tar.gz → 1.7.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 (66) hide show
  1. {veritensor-1.6.3/src/veritensor.egg-info → veritensor-1.7.0}/PKG-INFO +23 -9
  2. {veritensor-1.6.3 → veritensor-1.7.0}/README.md +20 -8
  3. {veritensor-1.6.3 → veritensor-1.7.0}/pyproject.toml +3 -1
  4. veritensor-1.7.0/src/veritensor/__init__.py +2 -0
  5. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/cli/main.py +134 -50
  6. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/config.py +6 -4
  7. veritensor-1.7.0/src/veritensor/core/networking.py +59 -0
  8. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/safe_zip.py +9 -3
  9. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/streaming.py +7 -6
  10. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/types.py +1 -1
  11. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/data/dataset_engine.py +72 -32
  12. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/data/excel_engine.py +1 -0
  13. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/hashing/readers.py +19 -14
  14. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/dependency_engine.py +18 -3
  15. veritensor-1.7.0/src/veritensor/engines/static/mcp_scanner.py +355 -0
  16. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/notebook_engine.py +17 -0
  17. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/pickle_engine.py +16 -6
  18. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/signatures.yaml +1 -1
  19. veritensor-1.7.0/src/veritensor/integrations/enterprise_scanner.py +159 -0
  20. veritensor-1.7.0/src/veritensor/reporting/html_report.py +351 -0
  21. veritensor-1.7.0/src/veritensor/reporting/sbom.py +191 -0
  22. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/reporting/telemetry.py +3 -2
  23. {veritensor-1.6.3 → veritensor-1.7.0/src/veritensor.egg-info}/PKG-INFO +23 -9
  24. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor.egg-info/SOURCES.txt +2 -1
  25. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor.egg-info/requires.txt +2 -0
  26. {veritensor-1.6.3 → veritensor-1.7.0}/veritensor.yaml +4 -12
  27. veritensor-1.6.3/src/veritensor/__init__.py +0 -2
  28. veritensor-1.6.3/src/veritensor/core/networking.py +0 -61
  29. veritensor-1.6.3/src/veritensor/core/utils.py +0 -43
  30. veritensor-1.6.3/src/veritensor/integrations/enterprise_scanner.py +0 -62
  31. veritensor-1.6.3/src/veritensor/reporting/sbom.py +0 -34
  32. {veritensor-1.6.3 → veritensor-1.7.0}/LICENSE +0 -0
  33. {veritensor-1.6.3 → veritensor-1.7.0}/MANIFEST.in +0 -0
  34. {veritensor-1.6.3 → veritensor-1.7.0}/setup.cfg +0 -0
  35. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/__main__.py +0 -0
  36. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/cli/__init__.py +0 -0
  37. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/__init__.py +0 -0
  38. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/cache.py +0 -0
  39. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/entropy.py +0 -0
  40. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/file_utils.py +0 -0
  41. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/text_utils.py +0 -0
  42. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/__init__.py +0 -0
  43. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/container/__init__.py +0 -0
  44. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/container/archive_engine.py +0 -0
  45. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/content/__init__.py +0 -0
  46. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/content/injection.py +0 -0
  47. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/content/pii.py +0 -0
  48. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/data/__init__.py +0 -0
  49. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/hashing/__init__.py +0 -0
  50. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/hashing/calculator.py +0 -0
  51. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/hashing/lfs.py +0 -0
  52. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/__init__.py +0 -0
  53. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/keras_engine.py +0 -0
  54. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/rules.py +0 -0
  55. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/__init__.py +0 -0
  56. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/chroma_guard.py +0 -0
  57. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/cosign.py +0 -0
  58. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/huggingface.py +0 -0
  59. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/langchain_guard.py +0 -0
  60. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/llamaindex_guard.py +0 -0
  61. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/unstructured_guard.py +0 -0
  62. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/reporting/manifest.py +0 -0
  63. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/reporting/sarif.py +0 -0
  64. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor.egg-info/dependency_links.txt +0 -0
  65. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor.egg-info/entry_points.txt +0 -0
  66. {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veritensor
3
- Version: 1.6.3
3
+ Version: 1.7.0
4
4
  Summary: Antivirus for the AI Supply Chain. Scans models, datasets, notebooks, and RAG documents for threats.
5
5
  Author-email: Arsenii Brazhnyk <veritensor@gmail.com>
6
6
  License: Apache-2.0
@@ -27,6 +27,8 @@ Requires-Dist: requests>=2.31.0
27
27
  Requires-Dist: pyyaml>=6.0
28
28
  Requires-Dist: huggingface_hub>=0.19.0
29
29
  Requires-Dist: cyclonedx-python-lib>=6.0.0
30
+ Requires-Dist: jinja2>=3.1.0
31
+ Requires-Dist: pillow>=12.2.0
30
32
  Provides-Extra: test
31
33
  Requires-Dist: pytest>=7.0.0; extra == "test"
32
34
  Requires-Dist: pytest-mock>=3.10.0; extra == "test"
@@ -92,7 +94,7 @@ Unlike standard SAST tools (which focus on code), Veritensor understands the bin
92
94
  * **De-obfuscation Engine:** Automatically detects and decodes **Base64** strings to uncover hidden payloads (e.g., `SWdub3Jl...` -> `Ignore previous instructions`).
93
95
  * **Magic Number Validation:** Detects malware masquerading as safe files (e.g., an `.exe` renamed to `invoice.pdf`).
94
96
  * **Smart Filtering & Entropy Analysis:** Drastically reduces false positives in Jupyter Notebooks. Uses Shannon Entropy to find real, unknown API keys (WandB, Pinecone, Telegram) while ignoring safe UUIDs and standard imports.
95
-
97
+ * **CISO-Ready HTML Reports:** Generate beautiful, standalone HTML security reports with interactive charts and severity breakdowns using the `--html` flag.
96
98
  ---
97
99
 
98
100
  ## 📦 Installation
@@ -175,6 +177,12 @@ Check code cells, markdown, and saved outputs for threats:
175
177
  veritensor scan ./research/experiment.ipynb
176
178
  ```
177
179
 
180
+ ### 10. Generate a CISO-Friendly HTML Report
181
+ Create a standalone, interactive HTML dashboard of your scan results:
182
+ ```bash
183
+ veritensor scan ./project --html
184
+ ```
185
+
178
186
  **Example Output:**
179
187
  ```Text
180
188
  ╭────────────────────────────────╮
@@ -296,17 +304,23 @@ with DAG('secure_rag_ingestion', start_date=datetime(2026, 1, 1)) as dag:
296
304
 
297
305
  Veritensor supports industry-standard formats for integration with security dashboards and audit tools.
298
306
 
299
- ### 1. GitHub Security (SARIF)
307
+ ### 1. Interactive HTML Dashboard
308
+ Generate a visually rich, standalone HTML report designed for CISOs and security audits. Includes severity breakdowns, charts, and easy copy-to-clipboard functionality for Jira tickets.
309
+ ```bash
310
+ veritensor scan ./models --html
311
+ ```
312
+
313
+ ### 2. GitHub Security (SARIF)
300
314
  Generate a report compatible with GitHub Code Scanning:
301
315
  ```bash
302
316
  veritensor scan ./models --sarif > veritensor-report.sarif
303
317
  ```
304
- ### 2. Software Bill of Materials (SBOM)
318
+ ### 3. Software Bill of Materials (SBOM)
305
319
  Generate a CycloneDX v1.5 SBOM to inventory your AI assets:
306
320
  ```bash
307
321
  veritensor scan ./models --sbom > sbom.json
308
322
  ```
309
- ### 3. Raw JSON
323
+ ### 4. Raw JSON
310
324
  For custom parsers and SOAR automation:
311
325
  ```bash
312
326
  veritensor scan ./models --json
@@ -360,7 +374,7 @@ jobs:
360
374
  steps:
361
375
  - uses: actions/checkout@v4
362
376
  - name: Veritensor Scan
363
- uses: arsbr/Veritensor@v1.6.3
377
+ uses: arsbr/Veritensor@v1.7.0
364
378
  with:
365
379
  path: '.'
366
380
  jobs: '4'
@@ -371,7 +385,7 @@ Prevent committing malicious models to your repository. Add this to .pre-commit-
371
385
  ```yaml
372
386
  repos:
373
387
  - repo: https://github.com/arsbr/Veritensor
374
- rev: v1.6.3
388
+ rev: v1.7.0
375
389
  hooks:
376
390
  - id: veritensor-scan
377
391
  ```
@@ -399,8 +413,8 @@ veritensor_audit:
399
413
  | **Datasets** | `.parquet`, `.csv`, `.tsv`, `.jsonl`, `.ndjson`, `.ldjson` | Streaming Regex Scan (URLs, Injections, PII) |
400
414
  | **Notebooks** | `.ipynb` | JSON Structure Analysis + Code AST + Markdown Phishing |
401
415
  | **Documents** | `.pdf`, `.docx`, `.pptx`, `.txt`, `.md`, `.html` | DOM Extraction, Stealth/CSS Detection, PII |
402
- | **Archives** | `.zip`, `.tar`, `.gz`, `.tgz`, `.whl` | Recursive In-Memory Inspection |
403
- | **RAG Docs** | `requirements.txt`, `poetry.lock`, `Pipfile.lock` | Typosquatting, OSV.dev CVE Lookup |
416
+ | **Media & Archives** | `.png`, `.jpg`, `.zip`, `.tar`, `.gz`, `.whl` | EasyOCR, LSB Steganography, YARA (Enterprise) |
417
+ | **Supply Chain** | `requirements.txt`, `pyproject.toml`, `poetry.lock`, `Pipfile.lock` | Typosquatting, OSV.dev CVE Lookup |
404
418
 
405
419
  ---
406
420
 
@@ -34,7 +34,7 @@ Unlike standard SAST tools (which focus on code), Veritensor understands the bin
34
34
  * **De-obfuscation Engine:** Automatically detects and decodes **Base64** strings to uncover hidden payloads (e.g., `SWdub3Jl...` -> `Ignore previous instructions`).
35
35
  * **Magic Number Validation:** Detects malware masquerading as safe files (e.g., an `.exe` renamed to `invoice.pdf`).
36
36
  * **Smart Filtering & Entropy Analysis:** Drastically reduces false positives in Jupyter Notebooks. Uses Shannon Entropy to find real, unknown API keys (WandB, Pinecone, Telegram) while ignoring safe UUIDs and standard imports.
37
-
37
+ * **CISO-Ready HTML Reports:** Generate beautiful, standalone HTML security reports with interactive charts and severity breakdowns using the `--html` flag.
38
38
  ---
39
39
 
40
40
  ## 📦 Installation
@@ -117,6 +117,12 @@ Check code cells, markdown, and saved outputs for threats:
117
117
  veritensor scan ./research/experiment.ipynb
118
118
  ```
119
119
 
120
+ ### 10. Generate a CISO-Friendly HTML Report
121
+ Create a standalone, interactive HTML dashboard of your scan results:
122
+ ```bash
123
+ veritensor scan ./project --html
124
+ ```
125
+
120
126
  **Example Output:**
121
127
  ```Text
122
128
  ╭────────────────────────────────╮
@@ -238,17 +244,23 @@ with DAG('secure_rag_ingestion', start_date=datetime(2026, 1, 1)) as dag:
238
244
 
239
245
  Veritensor supports industry-standard formats for integration with security dashboards and audit tools.
240
246
 
241
- ### 1. GitHub Security (SARIF)
247
+ ### 1. Interactive HTML Dashboard
248
+ Generate a visually rich, standalone HTML report designed for CISOs and security audits. Includes severity breakdowns, charts, and easy copy-to-clipboard functionality for Jira tickets.
249
+ ```bash
250
+ veritensor scan ./models --html
251
+ ```
252
+
253
+ ### 2. GitHub Security (SARIF)
242
254
  Generate a report compatible with GitHub Code Scanning:
243
255
  ```bash
244
256
  veritensor scan ./models --sarif > veritensor-report.sarif
245
257
  ```
246
- ### 2. Software Bill of Materials (SBOM)
258
+ ### 3. Software Bill of Materials (SBOM)
247
259
  Generate a CycloneDX v1.5 SBOM to inventory your AI assets:
248
260
  ```bash
249
261
  veritensor scan ./models --sbom > sbom.json
250
262
  ```
251
- ### 3. Raw JSON
263
+ ### 4. Raw JSON
252
264
  For custom parsers and SOAR automation:
253
265
  ```bash
254
266
  veritensor scan ./models --json
@@ -302,7 +314,7 @@ jobs:
302
314
  steps:
303
315
  - uses: actions/checkout@v4
304
316
  - name: Veritensor Scan
305
- uses: arsbr/Veritensor@v1.6.3
317
+ uses: arsbr/Veritensor@v1.7.0
306
318
  with:
307
319
  path: '.'
308
320
  jobs: '4'
@@ -313,7 +325,7 @@ Prevent committing malicious models to your repository. Add this to .pre-commit-
313
325
  ```yaml
314
326
  repos:
315
327
  - repo: https://github.com/arsbr/Veritensor
316
- rev: v1.6.3
328
+ rev: v1.7.0
317
329
  hooks:
318
330
  - id: veritensor-scan
319
331
  ```
@@ -341,8 +353,8 @@ veritensor_audit:
341
353
  | **Datasets** | `.parquet`, `.csv`, `.tsv`, `.jsonl`, `.ndjson`, `.ldjson` | Streaming Regex Scan (URLs, Injections, PII) |
342
354
  | **Notebooks** | `.ipynb` | JSON Structure Analysis + Code AST + Markdown Phishing |
343
355
  | **Documents** | `.pdf`, `.docx`, `.pptx`, `.txt`, `.md`, `.html` | DOM Extraction, Stealth/CSS Detection, PII |
344
- | **Archives** | `.zip`, `.tar`, `.gz`, `.tgz`, `.whl` | Recursive In-Memory Inspection |
345
- | **RAG Docs** | `requirements.txt`, `poetry.lock`, `Pipfile.lock` | Typosquatting, OSV.dev CVE Lookup |
356
+ | **Media & Archives** | `.png`, `.jpg`, `.zip`, `.tar`, `.gz`, `.whl` | EasyOCR, LSB Steganography, YARA (Enterprise) |
357
+ | **Supply Chain** | `requirements.txt`, `pyproject.toml`, `poetry.lock`, `Pipfile.lock` | Typosquatting, OSV.dev CVE Lookup |
346
358
 
347
359
  ---
348
360
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "veritensor"
7
- version = "1.6.3"
7
+ version = "1.7.0"
8
8
  description = "Antivirus for the AI Supply Chain. Scans models, datasets, notebooks, and RAG documents for threats."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -32,6 +32,8 @@ dependencies = [
32
32
  "pyyaml>=6.0",
33
33
  "huggingface_hub>=0.19.0",
34
34
  "cyclonedx-python-lib>=6.0.0",
35
+ "jinja2>=3.1.0",
36
+ "pillow>=12.2.0",
35
37
  # Fickling removed from here to avoid installing PyTorch (800MB+) for regular users
36
38
  ]
37
39
 
@@ -0,0 +1,2 @@
1
+ __version__ = "1.7.0"
2
+
@@ -36,16 +36,18 @@ from veritensor.engines.content.injection import scan_document, TEXT_EXTENSIONS,
36
36
  from veritensor.engines.static.notebook_engine import scan_notebook
37
37
  from veritensor.engines.data.dataset_engine import scan_dataset
38
38
  from veritensor.engines.static.dependency_engine import scan_dependencies
39
- from veritensor.engines.data.excel_engine import scan_excel # <--- NEW
40
- from veritensor.engines.container.archive_engine import scan_archive # <--- NEW
39
+ from veritensor.engines.data.excel_engine import scan_excel
40
+ from veritensor.engines.container.archive_engine import scan_archive #
41
41
 
42
42
  # Reporting
43
43
  from veritensor.reporting.telemetry import send_report
44
44
  from veritensor.reporting.sarif import generate_sarif_report
45
45
  from veritensor.reporting.sbom import generate_sbom
46
- from veritensor.reporting.manifest import generate_manifest # <--- NEW
46
+ from veritensor.reporting.manifest import generate_manifest
47
+ from veritensor.reporting.html_report import generate_html_report
47
48
 
48
49
  from veritensor.integrations.enterprise_scanner import EnterpriseScanner
50
+ from veritensor.engines.static.mcp_scanner import scan_mcp_server
49
51
 
50
52
  # Robust import for rules
51
53
  try:
@@ -72,8 +74,8 @@ PICKLE_EXTS = {".pt", ".pth", ".bin", ".pkl", ".ckpt", ".whl"}
72
74
  KERAS_EXTS = {".h5", ".keras"}
73
75
  NOTEBOOK_EXTS = {".ipynb"}
74
76
  DATASET_EXTS = {".parquet", ".csv", ".jsonl", ".tsv", ".ndjson"}
75
- EXCEL_EXTS = {".xlsx", ".xlsm", ".xltx"} # <--- NEW
76
- ARCHIVE_EXTS = {".zip", ".tar", ".gz", ".tgz"} # <--- NEW
77
+ EXCEL_EXTS = {".xlsx", ".xlsm", ".xltx"}
78
+ ARCHIVE_EXTS = {".zip", ".tar", ".gz", ".tgz", ".tar.gz"}
77
79
  DEP_FILES = {"requirements.txt", "pyproject.toml", "Pipfile", "poetry.lock", "Pipfile.lock"}
78
80
  ALL_DOC_EXTS = TEXT_EXTENSIONS.union(DOC_EXTS)
79
81
  CODE_EXTS = {".py",".js",".ts",".java",".c",".cpp",".cs",".go",".rs",".php",".rb",".swift",".kt",".m",".scala",".dart",".r",".jl",".hs",".clj",".ex",".exs",
@@ -89,6 +91,40 @@ NOISE_PATTERNS = [
89
91
  "Metadata parse error", "Suspicious script/XSS", "Suspicious link"
90
92
  ]
91
93
 
94
+ # Formats that we want to check on the server (OCR, YARA, Macros)
95
+ HEAVY_EXTS = {
96
+ # --- Images & Media (OCR, Steganography) ---
97
+ ".png", ".jpg", ".jpeg", ".tiff", ".bmp",
98
+
99
+ # --- Office & Documents (Macros, PDF parsing, RAG Security) ---
100
+ ".pdf", ".docx", ".pptx", ".xlsx", ".xlsm", ".xltx", ".docm",
101
+
102
+ # --- Archives (YARA, Zip Bomb protection) ---
103
+ ".zip", ".tar", ".gz", ".tgz", ".whl",
104
+
105
+ # --- AI Notebooks (AST, Secrets, Outputs) ---
106
+ ".ipynb",
107
+
108
+ # --- Datasets (Сэмплируются перед отправкой!) ---
109
+ ".parquet", ".csv", ".tsv", ".jsonl", ".ndjson",
110
+
111
+ # --- Text & Markup (DeBERTa Semantic Scan, GLiNER) ---
112
+ ".txt", ".md", ".markdown", ".rst", ".adoc", ".asciidoc",
113
+ ".tex", ".org", ".wiki", ".html", ".htm",
114
+
115
+ # --- Data & Configs (Secrets, PII) ---
116
+ ".json", ".xml", ".yaml", ".yml", ".toml",
117
+ ".ini", ".cfg", ".conf", ".env", ".properties",
118
+
119
+ # --- Infrastructure & Logs (High risk of PII and Secret leaks) ---
120
+ ".tf", ".tfvars", ".k8s", ".helm", ".tpl",
121
+ ".log", ".out", ".err"
122
+ }
123
+
124
+ # Specific files that are sent to the server (name verification)
125
+ HEAVY_FILES = {"dockerfile"}
126
+
127
+
92
128
  def load_ignore_patterns(ignore_file: str = ".veritensorignore") -> List[str]:
93
129
  """Loads glob patterns from .veritensorignore file."""
94
130
  patterns = []
@@ -102,11 +138,14 @@ def load_ignore_patterns(ignore_file: str = ".veritensorignore") -> List[str]:
102
138
  return patterns
103
139
 
104
140
  def is_ignored(file_path: Path, ignore_patterns: List[str]) -> bool:
105
- # Checks if a file matches any of the ignore patterns (supports directory matching)
141
+ path_str = str(file_path)
106
142
  for pattern in ignore_patterns:
107
- if file_path.match(pattern) or file_path.name == pattern:
143
+ if fnmatch.fnmatch(file_path.name, pattern):
108
144
  return True
109
- if any(p.match(pattern) or p.name == pattern for p in file_path.parents):
145
+ if fnmatch.fnmatch(path_str, f"*/{pattern}") or fnmatch.fnmatch(path_str, f"*/{pattern}/*"):
146
+ return True
147
+ # Direct comparison with a part of the path
148
+ if any(part == pattern for part in file_path.parts):
110
149
  return True
111
150
  return False
112
151
 
@@ -122,7 +161,7 @@ def check_remote_cache(report_url: str, api_key: str, hashes: List[str], version
122
161
 
123
162
  cache_url = report_url.replace("/telemetry", "/cache/check")
124
163
  headers = {"X-API-Key": api_key}
125
- payload = {"hashes": hashes, "scanner_version": __version__} # Added a version to the payload
164
+ payload = {"hashes": hashes, "scanner_version": version} # Added a version to the payload
126
165
 
127
166
  try:
128
167
  response = requests.post(cache_url, headers=headers, json=payload, timeout=5)
@@ -151,9 +190,14 @@ def fetch_server_policies(report_url: str, api_key: str) -> Tuple[Optional[dict]
151
190
  return None,[]
152
191
 
153
192
  def is_suppressed(file_path: str, threat_msg: str, suppressions: List[dict]) -> bool:
154
- # Checks if there is a threat in the server exclusion list.
155
193
  for supp in suppressions:
156
- if supp["file_path"] in file_path and supp["threat_type"] in threat_msg:
194
+ supp_path = supp.get("file_path", "")
195
+ supp_type = supp.get("threat_type", "")
196
+ # Require non-empty values and match by filename, not substring of full path
197
+ if not supp_path or not supp_type:
198
+ continue
199
+ file_name = Path(file_path).name
200
+ if file_name == supp_path and supp_type in threat_msg:
157
201
  return True
158
202
  return False
159
203
 
@@ -161,30 +205,28 @@ def is_suppressed(file_path: str, threat_msg: str, suppressions: List[dict]) ->
161
205
  def check_severity(threats: List[str], threshold: str) -> bool:
162
206
  threshold_val = SEVERITY_LEVELS.get(threshold.upper(), 4)
163
207
  for threat in threats:
164
- parts = threat.split(":")
165
- if len(parts) > 0:
166
- level_str = parts[0].strip().upper()
167
- level_val = SEVERITY_LEVELS.get(level_str, 3)
168
- if level_val >= threshold_val: return True
208
+ # Handle both "CRITICAL: ..." and "LINE 2: Semantic: CRITICAL: ..."
209
+ for part in threat.split(":"):
210
+ level_str = part.strip().upper()
211
+ level_val = SEVERITY_LEVELS.get(level_str, 0)
212
+ if level_val >= threshold_val:
213
+ return True
169
214
  return False
170
215
 
171
216
  # --- WORKER FUNCTION ---
172
- def scan_worker(args: Tuple[str, VeritensorConfig, Optional[str], bool, bool, bool]) -> ScanResult:
217
+ def scan_worker(args: Tuple[str, VeritensorConfig, Optional[str], bool, bool, bool, Optional[str]]) -> ScanResult:
173
218
  file_path_str, config, repo, ignore_license, full_scan_dataset, is_s3, precalc_hash = args
174
219
 
175
220
  if is_s3:
176
221
  file_name = file_path_str.split("/")[-1]
177
- # Using Path to extract ALL suffixes (e.g. .tar.gz )
178
- ext = "".join(Path(file_name).suffixes).lower()
179
- if not ext:
180
- ext = Path(file_name).suffix.lower()
181
222
  file_path = None
182
223
  else:
183
224
  file_path = Path(file_path_str)
184
225
  file_name = file_path.name
185
- ext = "".join(file_path.suffixes).lower()
186
- if not ext:
187
- ext = file_path.suffix.lower()
226
+ # Take the full extension (for example, .tar.gz ) and fallback to normal
227
+ ext = "".join(Path(file_name).suffixes).lower()
228
+ if ext not in ARCHIVE_EXTS and ext not in DATASET_EXTS:
229
+ ext = Path(file_name).suffix.lower()
188
230
 
189
231
  filename_lower = file_name.lower()
190
232
  scan_res = ScanResult(file_path=file_path_str)
@@ -193,20 +235,18 @@ def scan_worker(args: Tuple[str, VeritensorConfig, Optional[str], bool, bool, bo
193
235
  # --- HYBRID ROUTING (Sending heavy files to the server) ---
194
236
  # If the user has an Enterprise server connected
195
237
  if config.report_url and config.api_key and not is_s3 and file_path:
196
- # Formats that we want to check on the server (OCR, YARA, Macros)
197
- HEAVY_EXTS = {'.png', '.jpg', '.jpeg', '.xlsm', '.docm', '.pdf'}
198
238
 
199
- if ext in HEAVY_EXTS:
239
+
240
+ # Check if the extension OR the exact file name matches (in lowercase)
241
+ if ext in HEAVY_EXTS or filename_lower in HEAVY_FILES:
200
242
  try:
201
243
  scanner = EnterpriseScanner(config.report_url, config.api_key)
202
- remote_threats = scanner.scan_file_remotely(file_path)
244
+ remote_threats = scanner.scan_file_remotely(file_path, full_scan=full_scan_dataset)
203
245
 
204
246
  if remote_threats:
205
247
  for t in remote_threats: scan_res.add_threat(t)
206
248
 
207
- # If the file is checked on the server, we can skip the local checks.,
208
- # to avoid doing double work (especially for PDF).
209
- # We return the result immediately.
249
+ # Файл проверен на сервере (ML, OCR, YARA), пропускаем локальные проверки
210
250
  return scan_res
211
251
  except Exception as e:
212
252
  scan_res.add_threat(f"WARNING: Remote scan failed, falling back to local: {e}")
@@ -233,7 +273,7 @@ def scan_worker(args: Tuple[str, VeritensorConfig, Optional[str], bool, bool, bo
233
273
  try:
234
274
  if ext in PICKLE_EXTS:
235
275
  with get_stream_for_path(file_path_str) as f:
236
- threats = scan_pickle_stream(f, strict_mode=True)
276
+ threats = scan_pickle_stream(f, strict_mode=True, extra_allowed_modules=set(config.allowed_modules) if config.allowed_modules else None)
237
277
  for t in threats: scan_res.add_threat(t)
238
278
  elif ext in KERAS_EXTS:
239
279
  if is_s3: scan_res.add_threat("WARNING: S3 scanning not supported for Keras yet.")
@@ -279,7 +319,11 @@ def scan_worker(args: Tuple[str, VeritensorConfig, Optional[str], bool, bool, bo
279
319
  threats = scan_archive(file_path)
280
320
  for t in threats: scan_res.add_threat(t)
281
321
  elif ext in CODE_EXTS:
282
- pass
322
+ if ext == ".py" and file_path:
323
+ mcp_result = scan_mcp_server(file_path)
324
+ if mcp_result.mcp_tools_found:
325
+ for t in mcp_result.to_threat_strings():
326
+ scan_res.add_threat(t)
283
327
 
284
328
  else:
285
329
  # If the format is unknown to any engine at all.
@@ -354,11 +398,11 @@ def _run_scan_process(
354
398
  if jobs is None:
355
399
  try: jobs = multiprocessing.cpu_count()
356
400
  except NotImplementedError: jobs = 1
401
+ jobs = max(1, jobs)
357
402
  if len(files_to_scan) == 1: jobs = 1
358
403
 
359
404
  # 2. Prepare Tasks & Remote Cache Check
360
405
  tasks = []
361
- files_to_hash =[]
362
406
 
363
407
  # First, we quickly calculate the hashes locally (using SQLite so as not to read the files again)
364
408
  local_hashes_map = {} # {filepath: hash}
@@ -389,12 +433,12 @@ def _run_scan_process(
389
433
  for f in files_to_scan:
390
434
  is_s3 = str(f).startswith("s3://")
391
435
  if is_s3:
392
- tasks.append((str(f), config, repo, ignore_license, full_scan, True))
436
+ tasks.append((str(f), config, repo, ignore_license, full_scan, True, None))
393
437
  continue
394
438
 
395
439
  file_hash = local_hashes_map.get(str(f))
396
440
 
397
- # Если сервер знает этот файл, мы НЕ добавляем его в tasks!
441
+ # If the server knows this file, we DON'T add it to tasks
398
442
  if file_hash and file_hash in remote_cache_results:
399
443
  remote_data = remote_cache_results[file_hash]
400
444
  res = ScanResult(file_path=str(f), status=remote_data["status"], file_hash=file_hash)
@@ -406,6 +450,9 @@ def _run_scan_process(
406
450
 
407
451
  # 3. Execute
408
452
  executor = None
453
+ if not tasks:
454
+ hash_cache.close()
455
+ return results
409
456
  try:
410
457
  with Progress(
411
458
  SpinnerColumn(),
@@ -438,8 +485,11 @@ def _run_scan_process(
438
485
  results.append(err_res)
439
486
  progress.advance(main_task)
440
487
  finally:
441
- if executor: executor.shutdown(wait=True)
442
- hash_cache.close()
488
+ try:
489
+ if executor is not None:
490
+ executor.shutdown(wait=False, cancel_futures=True)
491
+ finally:
492
+ hash_cache.close()
443
493
 
444
494
  return results
445
495
 
@@ -458,25 +508,26 @@ def scan(
458
508
  report_to: Optional[str] = typer.Option(None, help="URL to send scan report (Enterprise)"),
459
509
  api_key: Optional[str] = typer.Option(None, envvar="VERITENSOR_API_KEY", help="API Key for reporting"),
460
510
  verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed logs"),
511
+ html_output: bool = typer.Option(False, "--html", help="Generate a beautiful HTML report"),
512
+ output_file: Optional[str] = typer.Option(None, "--output-file", "-o", help="Save machine-readable output (JSON/SARIF/SBOM) to a file instead of stdout"),
461
513
  ):
462
514
  """
463
515
  Scans models, data, and code for security threats.
464
516
  """
465
517
  config = ConfigLoader.load()
466
518
  if verbose: logger.setLevel(logging.DEBUG)
467
- is_machine_output = json_output or sarif_output or sbom_output
468
-
469
- # Прокидываем флаги из консоли в глобальный объект config
519
+ is_machine_output = (json_output or sarif_output or sbom_output) and not output_file
520
+
521
+ # Passing the flags from the console to the global config object
470
522
  config.report_url = report_to or config.report_url
471
523
  config.api_key = api_key or config.api_key
472
524
 
473
525
  # --- CENTRALIZED POLICY SYNC ---
474
526
  server_suppressions =[]
475
- if report_to and api_key:
527
+ if config.report_url and config.api_key:
476
528
  if not is_machine_output:
477
529
  console.print("[dim]🔄 Syncing policies from Veritensor Control Plane...[/dim]")
478
-
479
- server_config_dict, server_suppressions = fetch_server_policies(report_to, api_key)
530
+ server_config_dict, server_suppressions = fetch_server_policies(config.report_url, config.api_key)
480
531
 
481
532
  # If the server has returned the config, we overwrite the local settings.
482
533
  if server_config_dict:
@@ -531,20 +582,43 @@ def scan(
531
582
  clean_res = ScanResult(res.file_path, status="PASS")
532
583
  filtered_results.append(clean_res)
533
584
  else:
534
- # Оставляем только реальные угрозы для вывода
585
+ # Leave only real threats for withdrawal
535
586
  res.threats = real_threats
536
587
  filtered_results.append(res)
537
588
  else:
538
589
  filtered_results.append(res)
539
590
 
540
- if sarif_output: print(generate_sarif_report(results))
541
- elif sbom_output: print(generate_sbom(results))
591
+ if html_output:
592
+ report_path = generate_html_report(filtered_results)
593
+ if not is_machine_output:
594
+ console.print(f"\n[bold green]✅ HTML Report saved to: {report_path}[/bold green]")
595
+
596
+ machine_text = None
597
+ if sarif_output:
598
+ machine_text = generate_sarif_report(results)
599
+ elif sbom_output:
600
+ machine_text = generate_sbom(results)
542
601
  elif json_output:
543
- results_dicts =[r.__dict__ for r in results]
544
- print(json.dumps(results_dicts, indent=2))
602
+ results_dicts = [r.__dict__ for r in results]
603
+ machine_text = json.dumps(results_dicts, indent=2)
604
+
605
+
606
+ if machine_text:
607
+ if output_file:
608
+
609
+ with open(output_file, "w", encoding="utf-8") as f:
610
+ f.write(machine_text)
611
+ if not is_machine_output:
612
+ console.print(f"[bold green]✅ Report saved to: {output_file}[/bold green]")
613
+ if not is_machine_output:
614
+ _print_table(filtered_results)
615
+ else:
616
+ print(machine_text)
545
617
  else:
546
618
  _print_table(filtered_results)
547
619
 
620
+
621
+ # Telemetry
548
622
  if report_to or config.report_url:
549
623
  if not is_machine_output: console.print(f"[dim]📡 Sending telemetry...[/dim]")
550
624
  send_report(results, config, override_url=report_to, override_key=api_key)
@@ -570,6 +644,15 @@ def scan(
570
644
  block_reasons.append("License")
571
645
  exit_code = 1
572
646
 
647
+ if not is_machine_output:
648
+ failing_files = [r for r in filtered_results if r.status == "FAIL"]
649
+ if failing_files and exit_code == 0:
650
+ console.print(
651
+ f"\n[yellow]ℹ️ {len(failing_files)} file(s) have findings below "
652
+ f"the blocking threshold ({config.fail_on_severity}). "
653
+ f"Lower the threshold in veritensor.yaml to block these.[/yellow]"
654
+ )
655
+
573
656
  if exit_code != 0:
574
657
  if not is_machine_output: console.print(f"\n[bold red]❌ BLOCKING DEPLOYMENT due to: {', '.join(block_reasons)}[/bold red]")
575
658
  raise typer.Exit(code=1)
@@ -602,6 +685,7 @@ def manifest(
602
685
 
603
686
  saved_path = generate_manifest(results, output)
604
687
  console.print(f"[green]✅ Manifest saved to: {saved_path}[/green]")
688
+
605
689
  def _print_table(results: List[ScanResult]):
606
690
  table = Table(title="🛡️ Veritensor Scan Report", header_style="bold magenta")
607
691
  table.add_column("File", style="cyan", no_wrap=True)
@@ -643,7 +727,7 @@ def keygen(output_prefix: str = "veritensor"):
643
727
 
644
728
  @app.command()
645
729
  def update():
646
- SIG_URL = "https://raw.githubusercontent.com/ArseniiBrazhnyk/Veritensor/main/src/veritensor/engines/static/signatures.yaml"
730
+ SIG_URL = "https://raw.githubusercontent.com/arsbr/Veritensor/main/src/veritensor/engines/static/signatures.yaml"
647
731
  target_dir = Path.home() / ".veritensor"
648
732
  target_file = target_dir / "signatures.yaml"
649
733
  console.print(f"⬇️ Checking for updates...")
@@ -8,12 +8,13 @@
8
8
 
9
9
  import os
10
10
  import logging
11
- import yaml # Now a hard dependency
11
+ import yaml
12
12
  from pathlib import Path
13
13
  from dataclasses import dataclass, field
14
14
  from typing import List, Optional, Set
15
+ from veritensor import __version__
15
16
 
16
- VERSION = "1.6.3"
17
+ VERSION = __version__
17
18
  logger = logging.getLogger(__name__)
18
19
  DEFAULT_CONFIG_PATH = Path("veritensor.yaml")
19
20
 
@@ -21,7 +22,7 @@ DEFAULT_CONFIG_PATH = Path("veritensor.yaml")
21
22
  class VeritensorConfig:
22
23
  allowed_modules: List[str] = field(default_factory=list)
23
24
  ignored_rules: List[str] = field(default_factory=list)
24
- fail_on_severity: str = "CRITICAL"
25
+ fail_on_severity: str = "CRITICAL"
25
26
  hf_token: Optional[str] = None
26
27
  private_key_path: Optional[str] = None
27
28
  output_format: str = "table"
@@ -82,13 +83,14 @@ class ConfigLoader:
82
83
  private_key_path=config_data.get("private_key_path"),
83
84
  output_format=config_data.get("output_format", "table"),
84
85
  fail_on_missing_license=config_data.get("fail_on_missing_license", False),
85
- custom_restricted_licenses=config_data.get("restricted_licenses", []),
86
+ custom_restricted_licenses=config_data.get("custom_restricted_licenses", []),
86
87
  allowed_models=config_data.get("allowed_models", []),
87
88
  report_url=config_data.get("report_url"),
88
89
  api_key=config_data.get("api_key")
89
90
  )
90
91
  return cls._instance
91
92
 
93
+
92
94
  @classmethod
93
95
  def get_safe_modules(cls) -> Set[str]:
94
96
  from veritensor.engines.static.pickle_engine import SAFE_MODULES as DEFAULT_SAFE
@@ -0,0 +1,59 @@
1
+ # Veritensor 2026 Apache 2.0
2
+ import socket
3
+ import ipaddress
4
+ import logging
5
+ from urllib.parse import urlparse
6
+ from contextlib import contextmanager
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ _orig_getaddrinfo = socket.getaddrinfo
11
+
12
+ @contextmanager
13
+ def safe_dns_resolve(url: str):
14
+ """
15
+ A context manager for protection against SSRF and DNS Rebinding.
16
+ Resolves the IP, verifies its security, and accelerates usage
17
+ this IP is at the socket level, while maintaining valid TLS/SNI.
18
+ """
19
+ parsed = urlparse(url)
20
+ hostname = parsed.hostname
21
+
22
+ if not hostname:
23
+ yield # Local file or invalid URL
24
+ return
25
+
26
+ try:
27
+ ip_list = _orig_getaddrinfo(hostname, None)
28
+ safe_ip = None
29
+
30
+ for item in ip_list:
31
+ ip_addr = item[4][0]
32
+ ip_obj = ipaddress.ip_address(ip_addr)
33
+
34
+ if (ip_obj.is_private or ip_obj.is_loopback or
35
+ ip_obj.is_link_local or ip_obj.is_multicast or
36
+ ip_obj.is_reserved or ip_obj.is_unspecified):
37
+ raise ValueError(f"SSRF Protection: Access to private IP {ip_addr} is forbidden.")
38
+
39
+ if not safe_ip:
40
+ safe_ip = ip_addr
41
+
42
+ if not safe_ip:
43
+ raise ValueError(f"Could not resolve public IP for {hostname}")
44
+
45
+ # Перехватываем резолв DNS только для целевого хоста
46
+ def _safe_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
47
+ if host == hostname:
48
+ return _orig_getaddrinfo(safe_ip, port, family, type, proto, flags)
49
+ return _orig_getaddrinfo(host, port, family, type, proto, flags)
50
+
51
+ socket.getaddrinfo = _safe_getaddrinfo
52
+ try:
53
+ yield
54
+ finally:
55
+ socket.getaddrinfo = _orig_getaddrinfo
56
+
57
+ except socket.gaierror:
58
+ logger.warning(f"Could not resolve hostname: {hostname}")
59
+ yield