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.
- {veritensor-1.6.3/src/veritensor.egg-info → veritensor-1.7.0}/PKG-INFO +23 -9
- {veritensor-1.6.3 → veritensor-1.7.0}/README.md +20 -8
- {veritensor-1.6.3 → veritensor-1.7.0}/pyproject.toml +3 -1
- veritensor-1.7.0/src/veritensor/__init__.py +2 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/cli/main.py +134 -50
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/config.py +6 -4
- veritensor-1.7.0/src/veritensor/core/networking.py +59 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/safe_zip.py +9 -3
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/streaming.py +7 -6
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/types.py +1 -1
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/data/dataset_engine.py +72 -32
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/data/excel_engine.py +1 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/hashing/readers.py +19 -14
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/dependency_engine.py +18 -3
- veritensor-1.7.0/src/veritensor/engines/static/mcp_scanner.py +355 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/notebook_engine.py +17 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/pickle_engine.py +16 -6
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/signatures.yaml +1 -1
- veritensor-1.7.0/src/veritensor/integrations/enterprise_scanner.py +159 -0
- veritensor-1.7.0/src/veritensor/reporting/html_report.py +351 -0
- veritensor-1.7.0/src/veritensor/reporting/sbom.py +191 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/reporting/telemetry.py +3 -2
- {veritensor-1.6.3 → veritensor-1.7.0/src/veritensor.egg-info}/PKG-INFO +23 -9
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor.egg-info/SOURCES.txt +2 -1
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor.egg-info/requires.txt +2 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/veritensor.yaml +4 -12
- veritensor-1.6.3/src/veritensor/__init__.py +0 -2
- veritensor-1.6.3/src/veritensor/core/networking.py +0 -61
- veritensor-1.6.3/src/veritensor/core/utils.py +0 -43
- veritensor-1.6.3/src/veritensor/integrations/enterprise_scanner.py +0 -62
- veritensor-1.6.3/src/veritensor/reporting/sbom.py +0 -34
- {veritensor-1.6.3 → veritensor-1.7.0}/LICENSE +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/MANIFEST.in +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/setup.cfg +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/__main__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/cli/__init__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/__init__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/cache.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/entropy.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/file_utils.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/core/text_utils.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/__init__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/container/__init__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/container/archive_engine.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/content/__init__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/content/injection.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/content/pii.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/data/__init__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/hashing/__init__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/hashing/calculator.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/hashing/lfs.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/__init__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/keras_engine.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/engines/static/rules.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/__init__.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/chroma_guard.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/cosign.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/huggingface.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/langchain_guard.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/llamaindex_guard.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/integrations/unstructured_guard.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/reporting/manifest.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor/reporting/sarif.py +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor.egg-info/dependency_links.txt +0 -0
- {veritensor-1.6.3 → veritensor-1.7.0}/src/veritensor.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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.
|
|
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.
|
|
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`, `.
|
|
403
|
-
| **
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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.
|
|
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.
|
|
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`, `.
|
|
345
|
-
| **
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
40
|
-
from veritensor.engines.container.archive_engine import scan_archive #
|
|
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
|
|
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"}
|
|
76
|
-
ARCHIVE_EXTS = {".zip", ".tar", ".gz", ".tgz"}
|
|
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
|
-
|
|
141
|
+
path_str = str(file_path)
|
|
106
142
|
for pattern in ignore_patterns:
|
|
107
|
-
if
|
|
143
|
+
if fnmatch.fnmatch(file_path.name, pattern):
|
|
108
144
|
return True
|
|
109
|
-
if
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
level_str =
|
|
167
|
-
level_val = SEVERITY_LEVELS.get(level_str,
|
|
168
|
-
if level_val >= threshold_val:
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
|
541
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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 =
|
|
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("
|
|
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
|