pinhaul 0.4.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.
- pinhaul-0.4.0/.gitignore +30 -0
- pinhaul-0.4.0/PKG-INFO +115 -0
- pinhaul-0.4.0/README.md +100 -0
- pinhaul-0.4.0/pinhaul/__init__.py +15 -0
- pinhaul-0.4.0/pinhaul/cache.py +60 -0
- pinhaul-0.4.0/pinhaul/cli.py +644 -0
- pinhaul-0.4.0/pinhaul/client.py +153 -0
- pinhaul-0.4.0/pinhaul/config.py +74 -0
- pinhaul-0.4.0/pinhaul/runtime.py +57 -0
- pinhaul-0.4.0/pinhaul/scanners.py +114 -0
- pinhaul-0.4.0/pinhaul/transport.py +75 -0
- pinhaul-0.4.0/pyproject.toml +31 -0
- pinhaul-0.4.0/tests/test_cache.py +107 -0
- pinhaul-0.4.0/tests/test_scanners.py +78 -0
- pinhaul-0.4.0/tests/test_sdk.py +56 -0
pinhaul-0.4.0/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
|
|
9
|
+
# Env & secrets
|
|
10
|
+
.env
|
|
11
|
+
.env.local
|
|
12
|
+
|
|
13
|
+
# Node
|
|
14
|
+
node_modules/
|
|
15
|
+
|
|
16
|
+
# Django
|
|
17
|
+
**/static_root/
|
|
18
|
+
**/media/
|
|
19
|
+
db.sqlite3
|
|
20
|
+
|
|
21
|
+
# Tooling caches
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.pytest_cache/
|
|
24
|
+
uv.lock.disabled
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
|
|
29
|
+
# real deployment secrets
|
|
30
|
+
deploy/server.env
|
pinhaul-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pinhaul
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Runtime dependency inventory for Pinhaul: one import, your project reports what it actually runs.
|
|
5
|
+
Project-URL: Homepage, https://pinhaul.io
|
|
6
|
+
Author: Reknown
|
|
7
|
+
License: Proprietary
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: httpx>=0.24
|
|
10
|
+
Provides-Extra: dotenv
|
|
11
|
+
Requires-Dist: python-dotenv>=1.0; extra == 'dotenv'
|
|
12
|
+
Provides-Extra: scan
|
|
13
|
+
Requires-Dist: bandit>=1.7; extra == 'scan'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# pinhaul (Python SDK + CLI)
|
|
17
|
+
|
|
18
|
+
Runtime dependency security for vibe coders: one import (or one `docker run` with the
|
|
19
|
+
[agent](../agent)) and Pinhaul knows exactly what your project *actually* runs, scans it,
|
|
20
|
+
and tells you what matters in plain language.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install pinhaul # or: pip install "pinhaul[dotenv]" for .env loading
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 1. Get an API key
|
|
32
|
+
pinhaul register --email you@example.com
|
|
33
|
+
|
|
34
|
+
# 2. Add the key to your project
|
|
35
|
+
echo 'PINHAUL_API_KEY=<your key>' >> .env
|
|
36
|
+
|
|
37
|
+
# 3. One line in your app's entrypoint
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pinhaul import Pinhaul
|
|
42
|
+
Pinhaul(project_name="my-app", environment="prod", metadata={"team": "core"})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That's it. On startup, Pinhaul captures Python builtins (version, implementation,
|
|
46
|
+
platform, script name, hostname) and the package versions **actually loaded in your
|
|
47
|
+
process** — runtime truth, not a requirements.txt guess — and reports them to your
|
|
48
|
+
Pinhaul cloud in a background thread that can never crash or block your app.
|
|
49
|
+
|
|
50
|
+
## Snapshot caching (don't re-send what didn't change)
|
|
51
|
+
|
|
52
|
+
The SDK fingerprints the full payload (python facts + metadata + loaded inventory,
|
|
53
|
+
sorted so import order doesn't matter) and caches it under
|
|
54
|
+
`~/.cache/pinhaul/snapshots/`. An unchanged environment within the TTL sends
|
|
55
|
+
**nothing**; any change to python version, metadata, or a package version ships
|
|
56
|
+
immediately. After the TTL (default 6h) the next start re-sends once as a liveness
|
|
57
|
+
heartbeat — the server deduplicates that without creating rows.
|
|
58
|
+
|
|
59
|
+
| Env var | Default | Effect |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `PINHAUL_REPORT_TTL` | `21600` (6h) | Seconds an unchanged fingerprint is suppressed. `0` = always send |
|
|
62
|
+
| `PINHAUL_NO_CACHE` | unset | Set to disable client caching entirely |
|
|
63
|
+
|
|
64
|
+
`Pinhaul(..., force=True)` bypasses the cache for one explicit send. Server-side
|
|
65
|
+
dedup (same fingerprint within its window) protects against cache misses, CI, and
|
|
66
|
+
second machines regardless of client settings. Check what happened via
|
|
67
|
+
`ds.last_result`: `sent` / `deduplicated` / `skipped` / `disabled`.
|
|
68
|
+
|
|
69
|
+
## What it sends (and never sends)
|
|
70
|
+
|
|
71
|
+
✅ python version/implementation, platform, script *basename*, hostname, loaded package
|
|
72
|
+
names+versions, the metadata you explicitly pass.
|
|
73
|
+
|
|
74
|
+
❌ environment variables, file paths, source code, secrets, anything else.
|
|
75
|
+
|
|
76
|
+
Set `PINHAUL_DISABLE=1` to no-op (e.g. in CI). Set `PINHAUL_DEBUG=1` to print
|
|
77
|
+
diagnostics to stderr on failure.
|
|
78
|
+
|
|
79
|
+
## Local scan engines (feature-gated)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pinhaul scan --code # bandit SAST over your python code
|
|
83
|
+
pinhaul scan --secrets # gitleaks over your git history (full history, redacted)
|
|
84
|
+
pinhaul scan --code --secrets --project my-app
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `--code` needs bandit: `pip install "pinhaul[scan]"`
|
|
88
|
+
- `--secrets` needs the [gitleaks](https://github.com/zricethezav/gitleaks) binary on your PATH
|
|
89
|
+
- Engines are enabled per plan server-side; a disabled engine prints 🔒 instead of running
|
|
90
|
+
- **Privacy:** code snippets are never sent (bandit findings carry test id + file:line only);
|
|
91
|
+
gitleaks runs with `--redact` and secret values are stripped client-side *and* server-side
|
|
92
|
+
|
|
93
|
+
## CLI
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pinhaul doctor [--docker] # diagnose setup: API, key, cache, socket
|
|
97
|
+
pinhaul status # account + project health
|
|
98
|
+
pinhaul report [--project my-app] # severity summary + dependency intel + AI insight
|
|
99
|
+
pinhaul findings [--severity critical,high] [--source trivy,osv,bandit,gitleaks,depintel]
|
|
100
|
+
pinhaul mute 7643 [7644 ...] # hide accepted-risk findings from reports/alerts
|
|
101
|
+
pinhaul unmute 7643
|
|
102
|
+
pinhaul sbom --project my-app --out sbom.json # CycloneDX 1.5 from runtime inventory
|
|
103
|
+
pinhaul analyze [--project my-app] # queue an AI triage run
|
|
104
|
+
pinhaul init --name my-app # print the integration snippet
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
API endpoint defaults to `http://127.0.0.1:8000` for local development; override with
|
|
108
|
+
`PINHAUL_API_URL` or `--api-url`.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
cd sdk && python3 -m venv .venv && . .venv/bin/activate
|
|
114
|
+
pip install -e . && python -m unittest discover tests -v
|
|
115
|
+
```
|
pinhaul-0.4.0/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# pinhaul (Python SDK + CLI)
|
|
2
|
+
|
|
3
|
+
Runtime dependency security for vibe coders: one import (or one `docker run` with the
|
|
4
|
+
[agent](../agent)) and Pinhaul knows exactly what your project *actually* runs, scans it,
|
|
5
|
+
and tells you what matters in plain language.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pinhaul # or: pip install "pinhaul[dotenv]" for .env loading
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 1. Get an API key
|
|
17
|
+
pinhaul register --email you@example.com
|
|
18
|
+
|
|
19
|
+
# 2. Add the key to your project
|
|
20
|
+
echo 'PINHAUL_API_KEY=<your key>' >> .env
|
|
21
|
+
|
|
22
|
+
# 3. One line in your app's entrypoint
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from pinhaul import Pinhaul
|
|
27
|
+
Pinhaul(project_name="my-app", environment="prod", metadata={"team": "core"})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
That's it. On startup, Pinhaul captures Python builtins (version, implementation,
|
|
31
|
+
platform, script name, hostname) and the package versions **actually loaded in your
|
|
32
|
+
process** — runtime truth, not a requirements.txt guess — and reports them to your
|
|
33
|
+
Pinhaul cloud in a background thread that can never crash or block your app.
|
|
34
|
+
|
|
35
|
+
## Snapshot caching (don't re-send what didn't change)
|
|
36
|
+
|
|
37
|
+
The SDK fingerprints the full payload (python facts + metadata + loaded inventory,
|
|
38
|
+
sorted so import order doesn't matter) and caches it under
|
|
39
|
+
`~/.cache/pinhaul/snapshots/`. An unchanged environment within the TTL sends
|
|
40
|
+
**nothing**; any change to python version, metadata, or a package version ships
|
|
41
|
+
immediately. After the TTL (default 6h) the next start re-sends once as a liveness
|
|
42
|
+
heartbeat — the server deduplicates that without creating rows.
|
|
43
|
+
|
|
44
|
+
| Env var | Default | Effect |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `PINHAUL_REPORT_TTL` | `21600` (6h) | Seconds an unchanged fingerprint is suppressed. `0` = always send |
|
|
47
|
+
| `PINHAUL_NO_CACHE` | unset | Set to disable client caching entirely |
|
|
48
|
+
|
|
49
|
+
`Pinhaul(..., force=True)` bypasses the cache for one explicit send. Server-side
|
|
50
|
+
dedup (same fingerprint within its window) protects against cache misses, CI, and
|
|
51
|
+
second machines regardless of client settings. Check what happened via
|
|
52
|
+
`ds.last_result`: `sent` / `deduplicated` / `skipped` / `disabled`.
|
|
53
|
+
|
|
54
|
+
## What it sends (and never sends)
|
|
55
|
+
|
|
56
|
+
✅ python version/implementation, platform, script *basename*, hostname, loaded package
|
|
57
|
+
names+versions, the metadata you explicitly pass.
|
|
58
|
+
|
|
59
|
+
❌ environment variables, file paths, source code, secrets, anything else.
|
|
60
|
+
|
|
61
|
+
Set `PINHAUL_DISABLE=1` to no-op (e.g. in CI). Set `PINHAUL_DEBUG=1` to print
|
|
62
|
+
diagnostics to stderr on failure.
|
|
63
|
+
|
|
64
|
+
## Local scan engines (feature-gated)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pinhaul scan --code # bandit SAST over your python code
|
|
68
|
+
pinhaul scan --secrets # gitleaks over your git history (full history, redacted)
|
|
69
|
+
pinhaul scan --code --secrets --project my-app
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- `--code` needs bandit: `pip install "pinhaul[scan]"`
|
|
73
|
+
- `--secrets` needs the [gitleaks](https://github.com/zricethezav/gitleaks) binary on your PATH
|
|
74
|
+
- Engines are enabled per plan server-side; a disabled engine prints 🔒 instead of running
|
|
75
|
+
- **Privacy:** code snippets are never sent (bandit findings carry test id + file:line only);
|
|
76
|
+
gitleaks runs with `--redact` and secret values are stripped client-side *and* server-side
|
|
77
|
+
|
|
78
|
+
## CLI
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pinhaul doctor [--docker] # diagnose setup: API, key, cache, socket
|
|
82
|
+
pinhaul status # account + project health
|
|
83
|
+
pinhaul report [--project my-app] # severity summary + dependency intel + AI insight
|
|
84
|
+
pinhaul findings [--severity critical,high] [--source trivy,osv,bandit,gitleaks,depintel]
|
|
85
|
+
pinhaul mute 7643 [7644 ...] # hide accepted-risk findings from reports/alerts
|
|
86
|
+
pinhaul unmute 7643
|
|
87
|
+
pinhaul sbom --project my-app --out sbom.json # CycloneDX 1.5 from runtime inventory
|
|
88
|
+
pinhaul analyze [--project my-app] # queue an AI triage run
|
|
89
|
+
pinhaul init --name my-app # print the integration snippet
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
API endpoint defaults to `http://127.0.0.1:8000` for local development; override with
|
|
93
|
+
`PINHAUL_API_URL` or `--api-url`.
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cd sdk && python3 -m venv .venv && . .venv/bin/activate
|
|
99
|
+
pip install -e . && python -m unittest discover tests -v
|
|
100
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Pinhaul SDK: runtime dependency inventory with a one-line import.
|
|
2
|
+
|
|
3
|
+
from pinhaul import Pinhaul
|
|
4
|
+
Pinhaul(project_name="my-app") # reads PINHAUL_API_KEY from env/.env
|
|
5
|
+
|
|
6
|
+
Sends: python version/implementation, platform, script name, hostname, and the
|
|
7
|
+
package versions actually loaded in this process. Never env vars, never file
|
|
8
|
+
paths, never code. Fails silently (PINHAUL_DEBUG=1 to see errors) and can be
|
|
9
|
+
fully disabled with PINHAUL_DISABLE=1.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pinhaul.client import Pinhaul
|
|
13
|
+
|
|
14
|
+
__version__ = "0.4.0"
|
|
15
|
+
__all__ = ["Pinhaul", "__version__"]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Local snapshot cache: skip shipping when the runtime fingerprint is unchanged.
|
|
2
|
+
|
|
3
|
+
Cache lives under XDG_CACHE_HOME (default ~/.cache)/pinhaul/snapshots, one small
|
|
4
|
+
JSON per (project, api_url). Every operation fails open: a read-only home, a
|
|
5
|
+
corrupt file, or a missing directory must never break the host application.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
CACHE_VERSION = 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _cache_dir() -> Path:
|
|
18
|
+
base = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
|
|
19
|
+
return Path(base) / "pinhaul" / "snapshots"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _cache_path(project_name: str, api_url: str) -> Path:
|
|
23
|
+
raw = f"{project_name}|{api_url}".encode()
|
|
24
|
+
digest = hashlib.sha256(raw).hexdigest()[:24]
|
|
25
|
+
return _cache_dir() / f"{digest}.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load(project_name: str, api_url: str) -> dict | None:
|
|
29
|
+
"""Return {"fingerprint": str, "sent_at": epoch_seconds} or None."""
|
|
30
|
+
try:
|
|
31
|
+
path = _cache_path(project_name, api_url)
|
|
32
|
+
if not path.is_file():
|
|
33
|
+
return None
|
|
34
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
35
|
+
if data.get("version") != CACHE_VERSION:
|
|
36
|
+
return None
|
|
37
|
+
if not isinstance(data.get("fingerprint"), str):
|
|
38
|
+
return None
|
|
39
|
+
return data
|
|
40
|
+
except Exception:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def save(project_name: str, api_url: str, fingerprint: str) -> None:
|
|
45
|
+
try:
|
|
46
|
+
path = _cache_path(project_name, api_url)
|
|
47
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
path.write_text(
|
|
49
|
+
json.dumps({"version": CACHE_VERSION, "fingerprint": fingerprint, "sent_at": time.time()}),
|
|
50
|
+
encoding="utf-8",
|
|
51
|
+
)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def clear(project_name: str, api_url: str) -> None:
|
|
57
|
+
try:
|
|
58
|
+
_cache_path(project_name, api_url).unlink(missing_ok=True)
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|