reconkit 0.1.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.
- reconkit-0.1.0/.gitignore +30 -0
- reconkit-0.1.0/CHANGELOG.md +21 -0
- reconkit-0.1.0/LICENSE +21 -0
- reconkit-0.1.0/PKG-INFO +108 -0
- reconkit-0.1.0/README.md +80 -0
- reconkit-0.1.0/pyproject.toml +56 -0
- reconkit-0.1.0/src/reconkit/__init__.py +22 -0
- reconkit-0.1.0/src/reconkit/cli.py +218 -0
- reconkit-0.1.0/src/reconkit/core/__init__.py +0 -0
- reconkit-0.1.0/src/reconkit/core/rate_limit.py +24 -0
- reconkit-0.1.0/src/reconkit/forensics/__init__.py +0 -0
- reconkit-0.1.0/src/reconkit/forensics/exif.py +16 -0
- reconkit-0.1.0/src/reconkit/forensics/hashing.py +22 -0
- reconkit-0.1.0/src/reconkit/forensics/identify.py +12 -0
- reconkit-0.1.0/src/reconkit/forensics/strings.py +47 -0
- reconkit-0.1.0/src/reconkit/noir/__init__.py +54 -0
- reconkit-0.1.0/src/reconkit/noir/errors.py +29 -0
- reconkit-0.1.0/src/reconkit/noir/primitives.py +154 -0
- reconkit-0.1.0/src/reconkit/noir/public_api.py +221 -0
- reconkit-0.1.0/src/reconkit/osint/__init__.py +0 -0
- reconkit-0.1.0/src/reconkit/osint/dns.py +18 -0
- reconkit-0.1.0/src/reconkit/osint/ip.py +25 -0
- reconkit-0.1.0/src/reconkit/osint/whois.py +14 -0
- reconkit-0.1.0/src/reconkit/scraping/__init__.py +0 -0
- reconkit-0.1.0/src/reconkit/scraping/extract.py +36 -0
- reconkit-0.1.0/src/reconkit/scraping/fetch.py +81 -0
- reconkit-0.1.0/src/reconkit/scraping/robots.py +60 -0
- reconkit-0.1.0/tests/test_extract_links.py +13 -0
- reconkit-0.1.0/tests/test_hashing.py +9 -0
- reconkit-0.1.0/tests/test_noir_output_contract.py +23 -0
- reconkit-0.1.0/tests/test_noir_primitives.py +12 -0
- reconkit-0.1.0/tests/test_strings.py +11 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
.eggs/
|
|
14
|
+
|
|
15
|
+
# Virtual environments
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
ENV/
|
|
19
|
+
|
|
20
|
+
# Test / tooling caches
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.coverage
|
|
25
|
+
htmlcov/
|
|
26
|
+
|
|
27
|
+
# OS / editor
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
.vscode/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
Format: loosely based on *Keep a Changelog*. Versioning: SemVer where practical.
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-02-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Noir Stack public primitives: `EvidenceArtifact`, `EvidenceBundle`, `ProvenanceRecord`, `VerificationResult`.
|
|
11
|
+
- Domain-specific errors that describe trust boundaries and provenance constraints.
|
|
12
|
+
- Audit-grade CLI vocabulary: `evidence`, `boundary`, `verify`.
|
|
13
|
+
- Structured JSON outputs include: `decision_basis`, `verification_status`, `provenance_confidence`.
|
|
14
|
+
- `robots.txt` compliance (default) and rate limiting for web acquisition.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Public-facing surface and CLI terminology refactored to evidence/verification framing.
|
|
18
|
+
|
|
19
|
+
### Notes
|
|
20
|
+
- This release is intentionally conservative.
|
|
21
|
+
- The system is capable of refusing to speculate. This is a feature.
|
reconkit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Noir Stack LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
reconkit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reconkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small, safe-by-default toolkit for scraping, OSINT, and file triage.
|
|
5
|
+
Author: reconkit
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: forensics,osint,scraping,triage
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
11
|
+
Requires-Dist: dnspython>=2.6
|
|
12
|
+
Requires-Dist: exifread>=3.0
|
|
13
|
+
Requires-Dist: filetype>=1.2
|
|
14
|
+
Requires-Dist: httpx>=0.27
|
|
15
|
+
Requires-Dist: ipwhois>=1.2
|
|
16
|
+
Requires-Dist: lxml>=5.1
|
|
17
|
+
Requires-Dist: python-whois>=0.9
|
|
18
|
+
Requires-Dist: rich>=13.7
|
|
19
|
+
Requires-Dist: typer>=0.12
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
22
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
26
|
+
Requires-Dist: twine>=5.1; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# reconkit (Judicium surface)
|
|
30
|
+
|
|
31
|
+
`reconkit` is a **conservative, safe-by-default** toolkit that exposes a Noir Stack / Hexarch / Judicium-flavored public API.
|
|
32
|
+
|
|
33
|
+
It is designed to produce **audit-grade structured outputs** that explicitly state:
|
|
34
|
+
- what was observed (evidence)
|
|
35
|
+
- how it was observed (provenance)
|
|
36
|
+
- what is and is not being asserted (verification limits)
|
|
37
|
+
|
|
38
|
+
This project is intended for lawful use on systems/data you own or have permission to assess.
|
|
39
|
+
|
|
40
|
+
## Noir Stack primitives
|
|
41
|
+
|
|
42
|
+
Public primitives:
|
|
43
|
+
- `EvidenceArtifact`: immutable bytes + provenance
|
|
44
|
+
- `EvidenceBundle`: a set of artifacts for joint assessment
|
|
45
|
+
- `ProvenanceRecord`: acquisition/derivation metadata + trust boundary
|
|
46
|
+
- `VerificationResult`: structured decision output
|
|
47
|
+
|
|
48
|
+
All structured outputs include:
|
|
49
|
+
- `decision_basis`
|
|
50
|
+
- `verification_status`
|
|
51
|
+
- `provenance_confidence`
|
|
52
|
+
|
|
53
|
+
Because this is Judicium, not divination.
|
|
54
|
+
|
|
55
|
+
## Guarantees
|
|
56
|
+
|
|
57
|
+
What the system will reliably do:
|
|
58
|
+
- Preserve evidence payload bytes as acquired/derived, and compute stable SHA-256 for artifacts.
|
|
59
|
+
- Emit structured, JSON-serializable outputs suitable for logging and later review.
|
|
60
|
+
- Respect declared acquisition constraints (e.g., `max_bytes`) and treat them as policy boundaries.
|
|
61
|
+
- Respect `robots.txt` by default for web acquisition.
|
|
62
|
+
|
|
63
|
+
## Assumptions
|
|
64
|
+
|
|
65
|
+
What the system assumes unless you override it:
|
|
66
|
+
- `trust_boundary` is declared explicitly for acquisitions. The tool will not invent one.
|
|
67
|
+
- Web acquisition is transport-level observation, not identity/authenticity proof.
|
|
68
|
+
- Public-internet boundary data (DNS/WHOIS/RDAP) is administrative metadata.
|
|
69
|
+
|
|
70
|
+
## Non-goals
|
|
71
|
+
|
|
72
|
+
What the system explicitly does not do:
|
|
73
|
+
- No anti-bot bypassing, fingerprint evasion, proxy-rotation orchestration, or credentialed scraping.
|
|
74
|
+
- No claims of attribution, intent, or identity. (You can do that in your report; the tool will not do it for you.)
|
|
75
|
+
- No “automatic truth.” The system is already busy being correct.
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python -m pip install -e ".[dev]"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## CLI quickstart (Noir terminology)
|
|
84
|
+
|
|
85
|
+
All commands emit audit-grade JSON.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Evidence acquisition / derivation
|
|
89
|
+
reconkit evidence acquire-web "https://example.com" --trust-boundary public-internet
|
|
90
|
+
reconkit evidence derive-links "https://example.com"
|
|
91
|
+
reconkit evidence derive-text "https://example.com"
|
|
92
|
+
|
|
93
|
+
# Boundary observations (OSINT-ish, but we say the quiet part out loud)
|
|
94
|
+
reconkit boundary dns example.com
|
|
95
|
+
reconkit boundary whois example.com
|
|
96
|
+
reconkit boundary ip 8.8.8.8
|
|
97
|
+
|
|
98
|
+
# Verification (informational)
|
|
99
|
+
reconkit verify file-hash .\somefile.bin --algo sha256
|
|
100
|
+
reconkit verify file-type .\somefile.bin
|
|
101
|
+
reconkit verify strings .\somefile.bin --min-len 6
|
|
102
|
+
reconkit verify exif .\photo.jpg
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Notes
|
|
106
|
+
|
|
107
|
+
- If acquisition is blocked by `robots.txt`, that is not a “bug.” It is a boundary condition.
|
|
108
|
+
- If you need features that reduce accountability, you are in the wrong repository. Convenient.
|
reconkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# reconkit (Judicium surface)
|
|
2
|
+
|
|
3
|
+
`reconkit` is a **conservative, safe-by-default** toolkit that exposes a Noir Stack / Hexarch / Judicium-flavored public API.
|
|
4
|
+
|
|
5
|
+
It is designed to produce **audit-grade structured outputs** that explicitly state:
|
|
6
|
+
- what was observed (evidence)
|
|
7
|
+
- how it was observed (provenance)
|
|
8
|
+
- what is and is not being asserted (verification limits)
|
|
9
|
+
|
|
10
|
+
This project is intended for lawful use on systems/data you own or have permission to assess.
|
|
11
|
+
|
|
12
|
+
## Noir Stack primitives
|
|
13
|
+
|
|
14
|
+
Public primitives:
|
|
15
|
+
- `EvidenceArtifact`: immutable bytes + provenance
|
|
16
|
+
- `EvidenceBundle`: a set of artifacts for joint assessment
|
|
17
|
+
- `ProvenanceRecord`: acquisition/derivation metadata + trust boundary
|
|
18
|
+
- `VerificationResult`: structured decision output
|
|
19
|
+
|
|
20
|
+
All structured outputs include:
|
|
21
|
+
- `decision_basis`
|
|
22
|
+
- `verification_status`
|
|
23
|
+
- `provenance_confidence`
|
|
24
|
+
|
|
25
|
+
Because this is Judicium, not divination.
|
|
26
|
+
|
|
27
|
+
## Guarantees
|
|
28
|
+
|
|
29
|
+
What the system will reliably do:
|
|
30
|
+
- Preserve evidence payload bytes as acquired/derived, and compute stable SHA-256 for artifacts.
|
|
31
|
+
- Emit structured, JSON-serializable outputs suitable for logging and later review.
|
|
32
|
+
- Respect declared acquisition constraints (e.g., `max_bytes`) and treat them as policy boundaries.
|
|
33
|
+
- Respect `robots.txt` by default for web acquisition.
|
|
34
|
+
|
|
35
|
+
## Assumptions
|
|
36
|
+
|
|
37
|
+
What the system assumes unless you override it:
|
|
38
|
+
- `trust_boundary` is declared explicitly for acquisitions. The tool will not invent one.
|
|
39
|
+
- Web acquisition is transport-level observation, not identity/authenticity proof.
|
|
40
|
+
- Public-internet boundary data (DNS/WHOIS/RDAP) is administrative metadata.
|
|
41
|
+
|
|
42
|
+
## Non-goals
|
|
43
|
+
|
|
44
|
+
What the system explicitly does not do:
|
|
45
|
+
- No anti-bot bypassing, fingerprint evasion, proxy-rotation orchestration, or credentialed scraping.
|
|
46
|
+
- No claims of attribution, intent, or identity. (You can do that in your report; the tool will not do it for you.)
|
|
47
|
+
- No “automatic truth.” The system is already busy being correct.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python -m pip install -e ".[dev]"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## CLI quickstart (Noir terminology)
|
|
56
|
+
|
|
57
|
+
All commands emit audit-grade JSON.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Evidence acquisition / derivation
|
|
61
|
+
reconkit evidence acquire-web "https://example.com" --trust-boundary public-internet
|
|
62
|
+
reconkit evidence derive-links "https://example.com"
|
|
63
|
+
reconkit evidence derive-text "https://example.com"
|
|
64
|
+
|
|
65
|
+
# Boundary observations (OSINT-ish, but we say the quiet part out loud)
|
|
66
|
+
reconkit boundary dns example.com
|
|
67
|
+
reconkit boundary whois example.com
|
|
68
|
+
reconkit boundary ip 8.8.8.8
|
|
69
|
+
|
|
70
|
+
# Verification (informational)
|
|
71
|
+
reconkit verify file-hash .\somefile.bin --algo sha256
|
|
72
|
+
reconkit verify file-type .\somefile.bin
|
|
73
|
+
reconkit verify strings .\somefile.bin --min-len 6
|
|
74
|
+
reconkit verify exif .\photo.jpg
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Notes
|
|
78
|
+
|
|
79
|
+
- If acquisition is blocked by `robots.txt`, that is not a “bug.” It is a boundary condition.
|
|
80
|
+
- If you need features that reduce accountability, you are in the wrong repository. Convenient.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "reconkit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A small, safe-by-default toolkit for scraping, OSINT, and file triage."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "reconkit" }]
|
|
13
|
+
keywords = ["osint", "forensics", "scraping", "triage"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"httpx>=0.27",
|
|
16
|
+
"typer>=0.12",
|
|
17
|
+
"rich>=13.7",
|
|
18
|
+
"beautifulsoup4>=4.12",
|
|
19
|
+
"lxml>=5.1",
|
|
20
|
+
"dnspython>=2.6",
|
|
21
|
+
"python-whois>=0.9",
|
|
22
|
+
"ipwhois>=1.2",
|
|
23
|
+
"exifread>=3.0",
|
|
24
|
+
"filetype>=1.2",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"build>=1.2",
|
|
30
|
+
"twine>=5.1",
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
"pytest-asyncio>=0.23",
|
|
33
|
+
"ruff>=0.4",
|
|
34
|
+
"mypy>=1.10",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
reconkit = "reconkit.cli:app"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/reconkit"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
addopts = "-q"
|
|
45
|
+
asyncio_mode = "auto"
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
|
|
50
|
+
[tool.mypy]
|
|
51
|
+
python_version = "3.10"
|
|
52
|
+
warn_return_any = true
|
|
53
|
+
warn_unused_ignores = true
|
|
54
|
+
no_implicit_optional = true
|
|
55
|
+
strict_optional = true
|
|
56
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""reconkit.
|
|
2
|
+
|
|
3
|
+
Public API: Noir Stack / Hexarch / Judicium surface lives under `reconkit.noir`.
|
|
4
|
+
Internal modules remain available but are not the supported public interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from reconkit.noir.primitives import ( # noqa: E402
|
|
10
|
+
EvidenceArtifact,
|
|
11
|
+
EvidenceBundle,
|
|
12
|
+
ProvenanceRecord,
|
|
13
|
+
VerificationResult,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"__version__",
|
|
18
|
+
"EvidenceArtifact",
|
|
19
|
+
"EvidenceBundle",
|
|
20
|
+
"ProvenanceRecord",
|
|
21
|
+
"VerificationResult",
|
|
22
|
+
]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from reconkit.noir import public_api
|
|
10
|
+
from reconkit.noir.primitives import VerificationResult, json_dumps_audit
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(add_completion=False, no_args_is_help=True)
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
evidence_app = typer.Typer(no_args_is_help=True)
|
|
16
|
+
boundary_app = typer.Typer(no_args_is_help=True)
|
|
17
|
+
verify_app = typer.Typer(no_args_is_help=True)
|
|
18
|
+
app.add_typer(evidence_app, name="evidence")
|
|
19
|
+
app.add_typer(boundary_app, name="boundary")
|
|
20
|
+
app.add_typer(verify_app, name="verify")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _emit(result: VerificationResult) -> None:
|
|
24
|
+
# Audit-grade requirement: stdout must be machine-parseable JSON.
|
|
25
|
+
# Rich is excellent at presentation and equally excellent at line-wrapping.
|
|
26
|
+
# We only need one of those properties.
|
|
27
|
+
typer.echo(json_dumps_audit(result.to_json_dict()))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@evidence_app.command("acquire-web")
|
|
31
|
+
def evidence_acquire_web(
|
|
32
|
+
url: str,
|
|
33
|
+
trust_boundary: str = typer.Option(
|
|
34
|
+
"public-internet",
|
|
35
|
+
help="Declared trust boundary for acquisition (audited, not inferred).",
|
|
36
|
+
),
|
|
37
|
+
max_bytes: int = typer.Option(2_000_000, help="Max download size in bytes"),
|
|
38
|
+
timeout_s: float = typer.Option(20.0, help="Timeout in seconds"),
|
|
39
|
+
rps: float = typer.Option(1.0, help="Max requests per second"),
|
|
40
|
+
obey_robots: bool = typer.Option(True, help="Respect robots.txt"),
|
|
41
|
+
include_payload: bool = typer.Option(False, help="Include payload in output (base64)."),
|
|
42
|
+
):
|
|
43
|
+
"""Acquire a web response as evidence.
|
|
44
|
+
|
|
45
|
+
Conservative note: this is acquisition, not authentication. The system is many things.
|
|
46
|
+
Omniscient is not currently one of them.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
artifact = public_api.acquire_web_artifact(
|
|
50
|
+
url,
|
|
51
|
+
trust_boundary=trust_boundary,
|
|
52
|
+
max_bytes=max_bytes,
|
|
53
|
+
timeout_s=timeout_s,
|
|
54
|
+
rps=rps,
|
|
55
|
+
obey_robots=obey_robots,
|
|
56
|
+
)
|
|
57
|
+
_emit(
|
|
58
|
+
VerificationResult(
|
|
59
|
+
verification_status="informational",
|
|
60
|
+
decision_basis="http-get acquisition within declared trust boundary",
|
|
61
|
+
provenance_confidence="medium",
|
|
62
|
+
subject={"url": url, "trust_boundary": trust_boundary},
|
|
63
|
+
findings={"artifact": artifact.to_json_dict(include_payload=include_payload)},
|
|
64
|
+
limitations=[
|
|
65
|
+
"Transport acquisition does not assert publisher identity or content authenticity.",
|
|
66
|
+
],
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@evidence_app.command("derive-links")
|
|
72
|
+
def evidence_derive_links(
|
|
73
|
+
url: str,
|
|
74
|
+
timeout_s: float = typer.Option(20.0, help="Timeout in seconds"),
|
|
75
|
+
rps: float = typer.Option(1.0, help="Max requests per second"),
|
|
76
|
+
obey_robots: bool = typer.Option(True, help="Respect robots.txt"),
|
|
77
|
+
include_payload: bool = typer.Option(False, help="Include payload in output (base64)."),
|
|
78
|
+
):
|
|
79
|
+
"""Acquire a web response and derive a link-list artifact."""
|
|
80
|
+
|
|
81
|
+
web = public_api.acquire_web_artifact(url, timeout_s=timeout_s, rps=rps, obey_robots=obey_robots)
|
|
82
|
+
derived = public_api.derive_links(web, base_url=url)
|
|
83
|
+
_emit(
|
|
84
|
+
VerificationResult(
|
|
85
|
+
verification_status="informational",
|
|
86
|
+
decision_basis="derived link extraction from acquired HTML",
|
|
87
|
+
provenance_confidence="medium",
|
|
88
|
+
subject={"url": url},
|
|
89
|
+
findings={
|
|
90
|
+
"source_artifact": web.to_json_dict(include_payload=False),
|
|
91
|
+
"derived_artifact": derived.to_json_dict(include_payload=include_payload),
|
|
92
|
+
},
|
|
93
|
+
limitations=[
|
|
94
|
+
"Link extraction is heuristic and may miss dynamically generated URLs.",
|
|
95
|
+
],
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@evidence_app.command("derive-text")
|
|
101
|
+
def evidence_derive_text(
|
|
102
|
+
url: str,
|
|
103
|
+
timeout_s: float = typer.Option(20.0, help="Timeout in seconds"),
|
|
104
|
+
rps: float = typer.Option(1.0, help="Max requests per second"),
|
|
105
|
+
obey_robots: bool = typer.Option(True, help="Respect robots.txt"),
|
|
106
|
+
include_payload: bool = typer.Option(False, help="Include payload in output (base64)."),
|
|
107
|
+
):
|
|
108
|
+
"""Acquire a web response and derive a readable-text artifact."""
|
|
109
|
+
|
|
110
|
+
web = public_api.acquire_web_artifact(url, timeout_s=timeout_s, rps=rps, obey_robots=obey_robots)
|
|
111
|
+
derived = public_api.derive_readable_text(web, base_url=url)
|
|
112
|
+
_emit(
|
|
113
|
+
VerificationResult(
|
|
114
|
+
verification_status="informational",
|
|
115
|
+
decision_basis="derived readable text from acquired HTML",
|
|
116
|
+
provenance_confidence="medium",
|
|
117
|
+
subject={"url": url},
|
|
118
|
+
findings={
|
|
119
|
+
"source_artifact": web.to_json_dict(include_payload=False),
|
|
120
|
+
"derived_artifact": derived.to_json_dict(include_payload=include_payload),
|
|
121
|
+
},
|
|
122
|
+
limitations=[
|
|
123
|
+
"Readability extraction removes scripts/styles and may omit critical context.",
|
|
124
|
+
],
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@boundary_app.command("dns")
|
|
130
|
+
def boundary_dns(domain: str, include_payload: bool = typer.Option(False, help="Include payload in output (base64).")):
|
|
131
|
+
"""Observe DNS answers across the public-internet boundary."""
|
|
132
|
+
|
|
133
|
+
artifact = public_api.boundary_dns(domain)
|
|
134
|
+
_emit(
|
|
135
|
+
VerificationResult(
|
|
136
|
+
verification_status="informational",
|
|
137
|
+
decision_basis="DNS resolution observation",
|
|
138
|
+
provenance_confidence="medium",
|
|
139
|
+
subject={"domain": domain, "trust_boundary": "public-internet"},
|
|
140
|
+
findings={"artifact": artifact.to_json_dict(include_payload=include_payload)},
|
|
141
|
+
limitations=[
|
|
142
|
+
"DNS is an administrative naming system; answers are not identity proofs.",
|
|
143
|
+
],
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@boundary_app.command("whois")
|
|
149
|
+
def boundary_whois(domain: str, include_payload: bool = typer.Option(False, help="Include payload in output (base64).")):
|
|
150
|
+
"""Observe WHOIS/RDAP administrative metadata across the public-internet boundary."""
|
|
151
|
+
|
|
152
|
+
artifact = public_api.boundary_whois(domain)
|
|
153
|
+
_emit(
|
|
154
|
+
VerificationResult(
|
|
155
|
+
verification_status="informational",
|
|
156
|
+
decision_basis="WHOIS observation",
|
|
157
|
+
provenance_confidence="low",
|
|
158
|
+
subject={"domain": domain, "trust_boundary": "public-internet"},
|
|
159
|
+
findings={"artifact": artifact.to_json_dict(include_payload=include_payload)},
|
|
160
|
+
limitations=[
|
|
161
|
+
"WHOIS fields vary by registrar/TLD and may be redacted or stale.",
|
|
162
|
+
],
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@boundary_app.command("ip")
|
|
168
|
+
def boundary_ip(ip: str, include_payload: bool = typer.Option(False, help="Include payload in output (base64).")):
|
|
169
|
+
"""Observe RDAP/ASN administrative metadata across the public-internet boundary."""
|
|
170
|
+
|
|
171
|
+
artifact = public_api.boundary_ip(ip)
|
|
172
|
+
_emit(
|
|
173
|
+
VerificationResult(
|
|
174
|
+
verification_status="informational",
|
|
175
|
+
decision_basis="RDAP/ASN observation",
|
|
176
|
+
provenance_confidence="medium",
|
|
177
|
+
subject={"ip": ip, "trust_boundary": "public-internet"},
|
|
178
|
+
findings={"artifact": artifact.to_json_dict(include_payload=include_payload)},
|
|
179
|
+
limitations=[
|
|
180
|
+
"Administrative allocations do not imply actor attribution.",
|
|
181
|
+
],
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@verify_app.command("file-hash")
|
|
187
|
+
def verify_file_hash(
|
|
188
|
+
path: Path,
|
|
189
|
+
algo: str = typer.Option("sha256", help="md5|sha1|sha256"),
|
|
190
|
+
):
|
|
191
|
+
"""Verify (informational): compute a cryptographic digest over file bytes."""
|
|
192
|
+
|
|
193
|
+
_emit(public_api.verify_file_hash(path, algo=algo))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@verify_app.command("file-type")
|
|
197
|
+
def verify_file_type(path: Path):
|
|
198
|
+
"""Verify (informational): identify file type by signature."""
|
|
199
|
+
|
|
200
|
+
_emit(public_api.verify_file_type(path))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@verify_app.command("strings")
|
|
204
|
+
def verify_strings(
|
|
205
|
+
path: Path,
|
|
206
|
+
min_len: int = typer.Option(6, "--min-len"),
|
|
207
|
+
max_strings: Optional[int] = typer.Option(None, "--max"),
|
|
208
|
+
):
|
|
209
|
+
"""Verify (informational): extract printable strings as a triage aid."""
|
|
210
|
+
|
|
211
|
+
_emit(public_api.verify_strings(path, min_len=min_len, max_strings=max_strings))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@verify_app.command("exif")
|
|
215
|
+
def verify_exif(path: Path):
|
|
216
|
+
"""Verify (informational): extract EXIF tags."""
|
|
217
|
+
|
|
218
|
+
_emit(public_api.verify_exif(path))
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class RateLimiter:
|
|
9
|
+
"""Simple, process-local rate limiter.
|
|
10
|
+
|
|
11
|
+
This intentionally stays basic (no jitter, no token bucket) to be easy to understand and
|
|
12
|
+
replicate.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
requests_per_second: float = 1.0
|
|
16
|
+
_next_allowed_at: float = 0.0
|
|
17
|
+
|
|
18
|
+
def wait(self) -> None:
|
|
19
|
+
if self.requests_per_second <= 0:
|
|
20
|
+
return
|
|
21
|
+
now = time.monotonic()
|
|
22
|
+
if now < self._next_allowed_at:
|
|
23
|
+
time.sleep(self._next_allowed_at - now)
|
|
24
|
+
self._next_allowed_at = time.monotonic() + (1.0 / self.requests_per_second)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import exifread
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def extract_exif(path: Path) -> dict[str, str]:
|
|
9
|
+
with path.open("rb") as f:
|
|
10
|
+
tags = exifread.process_file(f, details=False)
|
|
11
|
+
|
|
12
|
+
# normalize to plain strings
|
|
13
|
+
out: dict[str, str] = {}
|
|
14
|
+
for k, v in tags.items():
|
|
15
|
+
out[str(k)] = str(v)
|
|
16
|
+
return out
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_SUPPORTED = {"md5", "sha1", "sha256"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def hash_file(path: Path, *, algo: str = "sha256", chunk_size: int = 1024 * 1024) -> str:
|
|
11
|
+
algo = algo.lower()
|
|
12
|
+
if algo not in _SUPPORTED:
|
|
13
|
+
raise ValueError(f"Unsupported algo: {algo}. Choose one of: {sorted(_SUPPORTED)}")
|
|
14
|
+
|
|
15
|
+
h = hashlib.new(algo)
|
|
16
|
+
with path.open("rb") as f:
|
|
17
|
+
while True:
|
|
18
|
+
chunk = f.read(chunk_size)
|
|
19
|
+
if not chunk:
|
|
20
|
+
break
|
|
21
|
+
h.update(chunk)
|
|
22
|
+
return h.hexdigest()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import filetype
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def identify_file(path: Path) -> dict[str, str | None]:
|
|
9
|
+
kind = filetype.guess(str(path))
|
|
10
|
+
if kind is None:
|
|
11
|
+
return {"path": str(path), "ext": None, "mime": None}
|
|
12
|
+
return {"path": str(path), "ext": kind.extension, "mime": kind.mime}
|