stillrunning-pip 1.0.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.
- stillrunning_pip-1.0.0/PKG-INFO +139 -0
- stillrunning_pip-1.0.0/README.md +113 -0
- stillrunning_pip-1.0.0/pyproject.toml +42 -0
- stillrunning_pip-1.0.0/setup.cfg +4 -0
- stillrunning_pip-1.0.0/stillrunning_pip/__init__.py +11 -0
- stillrunning_pip-1.0.0/stillrunning_pip/cli.py +287 -0
- stillrunning_pip-1.0.0/stillrunning_pip/config.py +79 -0
- stillrunning_pip-1.0.0/stillrunning_pip.egg-info/PKG-INFO +139 -0
- stillrunning_pip-1.0.0/stillrunning_pip.egg-info/SOURCES.txt +10 -0
- stillrunning_pip-1.0.0/stillrunning_pip.egg-info/dependency_links.txt +1 -0
- stillrunning_pip-1.0.0/stillrunning_pip.egg-info/entry_points.txt +2 -0
- stillrunning_pip-1.0.0/stillrunning_pip.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stillrunning-pip
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Secure pip wrapper with supply chain attack protection
|
|
5
|
+
Author-email: "stillrunning.io" <hello@stillrunning.io>
|
|
6
|
+
Project-URL: Homepage, https://stillrunning.io
|
|
7
|
+
Project-URL: Documentation, https://stillrunning.io/docs
|
|
8
|
+
Project-URL: Repository, https://github.com/johhnyg/stillrunning-pip
|
|
9
|
+
Project-URL: Issues, https://github.com/johhnyg/stillrunning-pip/issues
|
|
10
|
+
Keywords: security,supply-chain,pip,malware,protection
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# stillrunning-pip
|
|
28
|
+
|
|
29
|
+
Secure pip wrapper that scans packages for supply chain attacks before installing.
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/stillrunning-pip/)
|
|
32
|
+
[](https://stillrunning.io)
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install stillrunning-pip
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
Use `stillrunning-pip` instead of `pip`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
stillrunning-pip install requests flask
|
|
46
|
+
stillrunning-pip install -r requirements.txt
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or create an alias:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Add to ~/.bashrc or ~/.zshrc
|
|
53
|
+
alias pip='stillrunning-pip'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Setup
|
|
57
|
+
|
|
58
|
+
Configure your token and preferences:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
stillrunning-pip --setup
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or create `~/.stillrunning/config.json` manually:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"token": "sr_your_token_here",
|
|
69
|
+
"block_dangerous": true,
|
|
70
|
+
"warn_suspicious": true,
|
|
71
|
+
"offline_mode": "warn"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Example Output
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
🛡️ stillrunning security scan
|
|
79
|
+
Checking 5 package(s)...
|
|
80
|
+
|
|
81
|
+
✅ CLEAN requests==2.31.0
|
|
82
|
+
✅ CLEAN flask==2.3.0
|
|
83
|
+
⚠️ WARNING sketchy-lib==1.0.0
|
|
84
|
+
→ Obfuscated code patterns detected
|
|
85
|
+
🚫 BLOCKED evil-pkg==0.1.0
|
|
86
|
+
→ Known malicious package (reverse shell)
|
|
87
|
+
|
|
88
|
+
❌ Installation blocked
|
|
89
|
+
1 dangerous package(s) detected
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Configuration Options
|
|
93
|
+
|
|
94
|
+
| Option | Default | Description |
|
|
95
|
+
|--------|---------|-------------|
|
|
96
|
+
| `token` | `""` | stillrunning.io API token for AI scanning |
|
|
97
|
+
| `block_dangerous` | `true` | Block installs for dangerous packages |
|
|
98
|
+
| `warn_suspicious` | `true` | Show warnings for suspicious packages |
|
|
99
|
+
| `offline_mode` | `"warn"` | Behavior when API unreachable: `warn`, `block`, `allow` |
|
|
100
|
+
| `timeout` | `30` | API timeout in seconds |
|
|
101
|
+
|
|
102
|
+
## Environment Variables
|
|
103
|
+
|
|
104
|
+
- `STILLRUNNING_TOKEN` — Override token from config
|
|
105
|
+
|
|
106
|
+
## Free vs Paid
|
|
107
|
+
|
|
108
|
+
| Feature | Free | With Token |
|
|
109
|
+
|---------|------|------------|
|
|
110
|
+
| Known malicious packages | Blocked | Blocked |
|
|
111
|
+
| Threat feed database | Checked | Checked |
|
|
112
|
+
| AI analysis of unknown packages | - | Yes |
|
|
113
|
+
| Scans per day | Unlimited (cached) | 100-10000 |
|
|
114
|
+
|
|
115
|
+
Get a token at [stillrunning.io/pricing](https://stillrunning.io/pricing)
|
|
116
|
+
|
|
117
|
+
## What It Detects
|
|
118
|
+
|
|
119
|
+
- **Known malicious packages** — Packages in our threat database (DPRK campaigns, typosquats, backdoors)
|
|
120
|
+
- **Typosquatting** — Packages with names similar to popular packages
|
|
121
|
+
- **AI-flagged packages** — Obfuscated code, credential harvesting, reverse shells
|
|
122
|
+
|
|
123
|
+
## Bypass (Not Recommended)
|
|
124
|
+
|
|
125
|
+
To bypass scanning for a single install:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install <package> # Use pip directly
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Uninstall
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip uninstall stillrunning-pip
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# stillrunning-pip
|
|
2
|
+
|
|
3
|
+
Secure pip wrapper that scans packages for supply chain attacks before installing.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/stillrunning-pip/)
|
|
6
|
+
[](https://stillrunning.io)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install stillrunning-pip
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Use `stillrunning-pip` instead of `pip`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
stillrunning-pip install requests flask
|
|
20
|
+
stillrunning-pip install -r requirements.txt
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or create an alias:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Add to ~/.bashrc or ~/.zshrc
|
|
27
|
+
alias pip='stillrunning-pip'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
Configure your token and preferences:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
stillrunning-pip --setup
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or create `~/.stillrunning/config.json` manually:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"token": "sr_your_token_here",
|
|
43
|
+
"block_dangerous": true,
|
|
44
|
+
"warn_suspicious": true,
|
|
45
|
+
"offline_mode": "warn"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Example Output
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
🛡️ stillrunning security scan
|
|
53
|
+
Checking 5 package(s)...
|
|
54
|
+
|
|
55
|
+
✅ CLEAN requests==2.31.0
|
|
56
|
+
✅ CLEAN flask==2.3.0
|
|
57
|
+
⚠️ WARNING sketchy-lib==1.0.0
|
|
58
|
+
→ Obfuscated code patterns detected
|
|
59
|
+
🚫 BLOCKED evil-pkg==0.1.0
|
|
60
|
+
→ Known malicious package (reverse shell)
|
|
61
|
+
|
|
62
|
+
❌ Installation blocked
|
|
63
|
+
1 dangerous package(s) detected
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Configuration Options
|
|
67
|
+
|
|
68
|
+
| Option | Default | Description |
|
|
69
|
+
|--------|---------|-------------|
|
|
70
|
+
| `token` | `""` | stillrunning.io API token for AI scanning |
|
|
71
|
+
| `block_dangerous` | `true` | Block installs for dangerous packages |
|
|
72
|
+
| `warn_suspicious` | `true` | Show warnings for suspicious packages |
|
|
73
|
+
| `offline_mode` | `"warn"` | Behavior when API unreachable: `warn`, `block`, `allow` |
|
|
74
|
+
| `timeout` | `30` | API timeout in seconds |
|
|
75
|
+
|
|
76
|
+
## Environment Variables
|
|
77
|
+
|
|
78
|
+
- `STILLRUNNING_TOKEN` — Override token from config
|
|
79
|
+
|
|
80
|
+
## Free vs Paid
|
|
81
|
+
|
|
82
|
+
| Feature | Free | With Token |
|
|
83
|
+
|---------|------|------------|
|
|
84
|
+
| Known malicious packages | Blocked | Blocked |
|
|
85
|
+
| Threat feed database | Checked | Checked |
|
|
86
|
+
| AI analysis of unknown packages | - | Yes |
|
|
87
|
+
| Scans per day | Unlimited (cached) | 100-10000 |
|
|
88
|
+
|
|
89
|
+
Get a token at [stillrunning.io/pricing](https://stillrunning.io/pricing)
|
|
90
|
+
|
|
91
|
+
## What It Detects
|
|
92
|
+
|
|
93
|
+
- **Known malicious packages** — Packages in our threat database (DPRK campaigns, typosquats, backdoors)
|
|
94
|
+
- **Typosquatting** — Packages with names similar to popular packages
|
|
95
|
+
- **AI-flagged packages** — Obfuscated code, credential harvesting, reverse shells
|
|
96
|
+
|
|
97
|
+
## Bypass (Not Recommended)
|
|
98
|
+
|
|
99
|
+
To bypass scanning for a single install:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install <package> # Use pip directly
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Uninstall
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pip uninstall stillrunning-pip
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "stillrunning-pip"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Secure pip wrapper with supply chain attack protection"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "stillrunning.io", email = "hello@stillrunning.io"}
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Security",
|
|
26
|
+
"Topic :: System :: Installation/Setup",
|
|
27
|
+
]
|
|
28
|
+
keywords = ["security", "supply-chain", "pip", "malware", "protection"]
|
|
29
|
+
requires-python = ">=3.8"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://stillrunning.io"
|
|
33
|
+
Documentation = "https://stillrunning.io/docs"
|
|
34
|
+
Repository = "https://github.com/johhnyg/stillrunning-pip"
|
|
35
|
+
Issues = "https://github.com/johhnyg/stillrunning-pip/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
stillrunning-pip = "stillrunning_pip.cli:main"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["."]
|
|
42
|
+
include = ["stillrunning_pip*"]
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
stillrunning-pip — Secure pip wrapper with supply chain attack protection.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
stillrunning-pip install requests flask
|
|
7
|
+
stillrunning-pip --setup
|
|
8
|
+
stillrunning-pip --version
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import urllib.request
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .config import load_config, setup_config
|
|
19
|
+
from . import __version__
|
|
20
|
+
|
|
21
|
+
# Terminal colors
|
|
22
|
+
RED = "\033[91m"
|
|
23
|
+
YELLOW = "\033[93m"
|
|
24
|
+
GREEN = "\033[92m"
|
|
25
|
+
BOLD = "\033[1m"
|
|
26
|
+
DIM = "\033[2m"
|
|
27
|
+
RESET = "\033[0m"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_packages(args: list) -> list:
|
|
31
|
+
"""Extract package names from pip install arguments."""
|
|
32
|
+
packages = []
|
|
33
|
+
skip_next = False
|
|
34
|
+
|
|
35
|
+
for arg in args:
|
|
36
|
+
if skip_next:
|
|
37
|
+
skip_next = False
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
if arg.startswith("-"):
|
|
41
|
+
# Some flags take values
|
|
42
|
+
if arg in ("-r", "--requirement", "-e", "--editable", "-t", "--target",
|
|
43
|
+
"-c", "--constraint", "-i", "--index-url", "--extra-index-url"):
|
|
44
|
+
skip_next = True
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if arg in ("install", "i"):
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# This is a package name (possibly with version specifier)
|
|
51
|
+
# Remove version specifiers for API
|
|
52
|
+
pkg = arg.strip()
|
|
53
|
+
if pkg:
|
|
54
|
+
packages.append(pkg)
|
|
55
|
+
|
|
56
|
+
return packages
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_requirements_file(filepath: str) -> list:
|
|
60
|
+
"""Parse a requirements.txt file."""
|
|
61
|
+
packages = []
|
|
62
|
+
try:
|
|
63
|
+
with open(filepath) as f:
|
|
64
|
+
for line in f:
|
|
65
|
+
line = line.strip()
|
|
66
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
67
|
+
continue
|
|
68
|
+
if "#" in line:
|
|
69
|
+
line = line.split("#")[0].strip()
|
|
70
|
+
if line:
|
|
71
|
+
packages.append(line)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
return packages
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def call_api(packages: list, config: dict) -> dict:
|
|
78
|
+
"""Call stillrunning.io API."""
|
|
79
|
+
# Format packages for API
|
|
80
|
+
package_list = []
|
|
81
|
+
for pkg in packages:
|
|
82
|
+
# Parse name and version
|
|
83
|
+
for sep in ["==", ">=", "<=", "~=", "!=", ">", "<", "@"]:
|
|
84
|
+
if sep in pkg:
|
|
85
|
+
parts = pkg.split(sep, 1)
|
|
86
|
+
package_list.append({
|
|
87
|
+
"name": parts[0].strip(),
|
|
88
|
+
"version": parts[1].strip() if len(parts) > 1 else "latest"
|
|
89
|
+
})
|
|
90
|
+
break
|
|
91
|
+
else:
|
|
92
|
+
# Remove extras like [security]
|
|
93
|
+
name = pkg.split("[")[0].strip() if "[" in pkg else pkg.strip()
|
|
94
|
+
package_list.append({"name": name, "version": "latest"})
|
|
95
|
+
|
|
96
|
+
payload = json.dumps({
|
|
97
|
+
"packages": package_list,
|
|
98
|
+
"token": config.get("token", "")
|
|
99
|
+
}).encode()
|
|
100
|
+
|
|
101
|
+
api_url = config.get("api_url", "https://stillrunning.io/api/pip-plugin/scan")
|
|
102
|
+
timeout = config.get("timeout", 30)
|
|
103
|
+
|
|
104
|
+
req = urllib.request.Request(
|
|
105
|
+
api_url,
|
|
106
|
+
data=payload,
|
|
107
|
+
headers={
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
"User-Agent": f"stillrunning-pip/{__version__}"
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
115
|
+
return json.loads(resp.read().decode())
|
|
116
|
+
except urllib.error.HTTPError as e:
|
|
117
|
+
error_body = e.read().decode() if e.fp else ""
|
|
118
|
+
return {"error": f"API error: {e.code}", "details": error_body}
|
|
119
|
+
except urllib.error.URLError as e:
|
|
120
|
+
return {"error": f"Network error: {e.reason}", "offline": True}
|
|
121
|
+
except Exception as e:
|
|
122
|
+
return {"error": str(e), "offline": True}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def print_result(result: dict):
|
|
126
|
+
"""Print formatted result for a package."""
|
|
127
|
+
verdict = result.get("verdict", "UNKNOWN")
|
|
128
|
+
package = result.get("package", "unknown")
|
|
129
|
+
version = result.get("version", "")
|
|
130
|
+
score = result.get("score", 0)
|
|
131
|
+
reason = result.get("reason", "")
|
|
132
|
+
|
|
133
|
+
pkg_display = f"{package}=={version}" if version and version != "latest" else package
|
|
134
|
+
|
|
135
|
+
if verdict == "DANGEROUS":
|
|
136
|
+
print(f" {RED}{BOLD}🚫 BLOCKED{RESET} {pkg_display}")
|
|
137
|
+
if reason:
|
|
138
|
+
print(f" {DIM}→ {reason}{RESET}")
|
|
139
|
+
elif verdict == "SUSPICIOUS":
|
|
140
|
+
print(f" {YELLOW}⚠️ WARNING{RESET} {pkg_display}")
|
|
141
|
+
if reason:
|
|
142
|
+
print(f" {DIM}→ {reason}{RESET}")
|
|
143
|
+
elif verdict == "UNKNOWN":
|
|
144
|
+
print(f" {DIM}❓ UNKNOWN{RESET} {pkg_display}")
|
|
145
|
+
if reason:
|
|
146
|
+
print(f" {DIM}→ {reason}{RESET}")
|
|
147
|
+
else:
|
|
148
|
+
print(f" {GREEN}✅ CLEAN{RESET} {pkg_display}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def main():
|
|
152
|
+
"""Main entry point."""
|
|
153
|
+
args = sys.argv[1:]
|
|
154
|
+
|
|
155
|
+
# Handle special flags
|
|
156
|
+
if not args or "--help" in args or "-h" in args:
|
|
157
|
+
print(f"""
|
|
158
|
+
{BOLD}stillrunning-pip{RESET} v{__version__} — Secure pip wrapper
|
|
159
|
+
|
|
160
|
+
Usage:
|
|
161
|
+
stillrunning-pip install <packages> Scan and install packages
|
|
162
|
+
stillrunning-pip --setup Configure stillrunning-pip
|
|
163
|
+
stillrunning-pip --version Show version
|
|
164
|
+
stillrunning-pip <pip-command> Pass through to pip
|
|
165
|
+
|
|
166
|
+
Examples:
|
|
167
|
+
stillrunning-pip install requests flask
|
|
168
|
+
stillrunning-pip install -r requirements.txt
|
|
169
|
+
""")
|
|
170
|
+
if args and args[0] not in ("--help", "-h"):
|
|
171
|
+
# Pass through to pip
|
|
172
|
+
result = subprocess.run(["pip"] + args)
|
|
173
|
+
sys.exit(result.returncode)
|
|
174
|
+
sys.exit(0)
|
|
175
|
+
|
|
176
|
+
if "--version" in args:
|
|
177
|
+
print(f"stillrunning-pip {__version__}")
|
|
178
|
+
sys.exit(0)
|
|
179
|
+
|
|
180
|
+
if "--setup" in args:
|
|
181
|
+
setup_config()
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
|
|
184
|
+
# Only intercept install commands
|
|
185
|
+
if args[0] not in ("install", "i"):
|
|
186
|
+
# Pass through non-install commands
|
|
187
|
+
result = subprocess.run(["pip"] + args)
|
|
188
|
+
sys.exit(result.returncode)
|
|
189
|
+
|
|
190
|
+
# Load config
|
|
191
|
+
config = load_config()
|
|
192
|
+
|
|
193
|
+
# Extract packages to check
|
|
194
|
+
packages = extract_packages(args)
|
|
195
|
+
|
|
196
|
+
# Check for -r flag and parse requirements file
|
|
197
|
+
for i, arg in enumerate(args):
|
|
198
|
+
if arg in ("-r", "--requirement") and i + 1 < len(args):
|
|
199
|
+
req_packages = parse_requirements_file(args[i + 1])
|
|
200
|
+
packages.extend(req_packages)
|
|
201
|
+
|
|
202
|
+
# Deduplicate
|
|
203
|
+
packages = list(set(packages))
|
|
204
|
+
|
|
205
|
+
if not packages:
|
|
206
|
+
# No packages to check, just pass through
|
|
207
|
+
result = subprocess.run(["pip"] + args)
|
|
208
|
+
sys.exit(result.returncode)
|
|
209
|
+
|
|
210
|
+
# Print header
|
|
211
|
+
print(f"\n{BOLD}🛡️ stillrunning security scan{RESET}")
|
|
212
|
+
print(f"{DIM} Checking {len(packages)} package(s)...{RESET}\n")
|
|
213
|
+
|
|
214
|
+
# Call API
|
|
215
|
+
api_result = call_api(packages, config)
|
|
216
|
+
|
|
217
|
+
# Handle errors
|
|
218
|
+
if "error" in api_result:
|
|
219
|
+
offline_mode = config.get("offline_mode", "warn")
|
|
220
|
+
|
|
221
|
+
if api_result.get("offline"):
|
|
222
|
+
if offline_mode == "block":
|
|
223
|
+
print(f"{RED}Error: API unreachable — install blocked{RESET}")
|
|
224
|
+
print(f"{DIM} {api_result['error']}{RESET}\n")
|
|
225
|
+
sys.exit(1)
|
|
226
|
+
elif offline_mode == "allow":
|
|
227
|
+
print(f"{DIM}API unavailable — proceeding without scan{RESET}\n")
|
|
228
|
+
result = subprocess.run(["pip"] + args)
|
|
229
|
+
sys.exit(result.returncode)
|
|
230
|
+
else: # warn
|
|
231
|
+
print(f"{YELLOW}⚠️ API unavailable — proceeding with caution{RESET}")
|
|
232
|
+
print(f"{DIM} {api_result['error']}{RESET}\n")
|
|
233
|
+
result = subprocess.run(["pip"] + args)
|
|
234
|
+
sys.exit(result.returncode)
|
|
235
|
+
else:
|
|
236
|
+
print(f"{RED}Error: {api_result['error']}{RESET}")
|
|
237
|
+
if api_result.get("details"):
|
|
238
|
+
print(f"{DIM} {api_result['details'][:200]}{RESET}")
|
|
239
|
+
print()
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
|
|
242
|
+
# Process results
|
|
243
|
+
results = api_result.get("results", [])
|
|
244
|
+
blocked = []
|
|
245
|
+
warnings = []
|
|
246
|
+
|
|
247
|
+
for r in results:
|
|
248
|
+
verdict = r.get("verdict", "CLEAN")
|
|
249
|
+
if verdict == "DANGEROUS":
|
|
250
|
+
blocked.append(r)
|
|
251
|
+
print_result(r)
|
|
252
|
+
elif verdict == "SUSPICIOUS":
|
|
253
|
+
warnings.append(r)
|
|
254
|
+
if config.get("warn_suspicious", True):
|
|
255
|
+
print_result(r)
|
|
256
|
+
elif verdict == "UNKNOWN":
|
|
257
|
+
print_result(r)
|
|
258
|
+
|
|
259
|
+
# Handle blocked packages
|
|
260
|
+
if blocked and config.get("block_dangerous", True):
|
|
261
|
+
print(f"\n{RED}{BOLD}❌ Installation blocked{RESET}")
|
|
262
|
+
print(f"{RED} {len(blocked)} dangerous package(s) detected{RESET}")
|
|
263
|
+
print(f"\n{DIM}If you believe this is a false positive, report it at:{RESET}")
|
|
264
|
+
print(f"{DIM}https://stillrunning.io/report{RESET}\n")
|
|
265
|
+
sys.exit(1)
|
|
266
|
+
|
|
267
|
+
# Handle warnings
|
|
268
|
+
if warnings and config.get("warn_suspicious", True):
|
|
269
|
+
print(f"\n{YELLOW}⚠️ {len(warnings)} suspicious package(s) found{RESET}")
|
|
270
|
+
|
|
271
|
+
# Show upgrade prompt if unknown packages and no token
|
|
272
|
+
unknown = [r for r in results if r.get("verdict") == "UNKNOWN"]
|
|
273
|
+
if unknown and not config.get("token"):
|
|
274
|
+
print(f"\n{DIM}💡 {len(unknown)} packages not AI-scanned.{RESET}")
|
|
275
|
+
print(f"{DIM} Get a token at https://stillrunning.io/pricing{RESET}")
|
|
276
|
+
|
|
277
|
+
# All clear — proceed with install
|
|
278
|
+
clean_count = len([r for r in results if r.get("verdict") == "CLEAN"])
|
|
279
|
+
print(f"\n{GREEN}✅ {clean_count} package(s) verified{RESET}")
|
|
280
|
+
print(f"{DIM} Proceeding with pip install...{RESET}\n")
|
|
281
|
+
|
|
282
|
+
result = subprocess.run(["pip"] + args)
|
|
283
|
+
sys.exit(result.returncode)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Configuration loading for stillrunning-pip."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
CONFIG_DIR = Path.home() / ".stillrunning"
|
|
7
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
8
|
+
|
|
9
|
+
DEFAULT_CONFIG = {
|
|
10
|
+
"token": "",
|
|
11
|
+
"block_dangerous": True,
|
|
12
|
+
"warn_suspicious": True,
|
|
13
|
+
"offline_mode": "warn", # "warn", "block", "allow"
|
|
14
|
+
"timeout": 30,
|
|
15
|
+
"api_url": "https://stillrunning.io/api/pip-plugin/scan"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_config() -> dict:
|
|
20
|
+
"""Load config from ~/.stillrunning/config.json"""
|
|
21
|
+
config = DEFAULT_CONFIG.copy()
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
if CONFIG_FILE.exists():
|
|
25
|
+
with open(CONFIG_FILE) as f:
|
|
26
|
+
user_config = json.load(f)
|
|
27
|
+
config.update(user_config)
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
# Environment variable overrides
|
|
32
|
+
if os.environ.get("STILLRUNNING_TOKEN"):
|
|
33
|
+
config["token"] = os.environ["STILLRUNNING_TOKEN"]
|
|
34
|
+
|
|
35
|
+
return config
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def save_config(config: dict):
|
|
39
|
+
"""Save config to ~/.stillrunning/config.json"""
|
|
40
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
with open(CONFIG_FILE, "w") as f:
|
|
43
|
+
json.dump(config, f, indent=2)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def setup_config():
|
|
47
|
+
"""Interactive config setup."""
|
|
48
|
+
print("\n" + "=" * 50)
|
|
49
|
+
print("stillrunning-pip Setup")
|
|
50
|
+
print("=" * 50 + "\n")
|
|
51
|
+
|
|
52
|
+
config = load_config()
|
|
53
|
+
|
|
54
|
+
# Token
|
|
55
|
+
print("Enter your stillrunning.io token (or press Enter to skip):")
|
|
56
|
+
print("Get a token at https://stillrunning.io/pricing")
|
|
57
|
+
token = input("> ").strip()
|
|
58
|
+
if token:
|
|
59
|
+
config["token"] = token
|
|
60
|
+
|
|
61
|
+
# Block dangerous
|
|
62
|
+
print("\nBlock installs for dangerous packages? [Y/n]")
|
|
63
|
+
response = input("> ").strip().lower()
|
|
64
|
+
config["block_dangerous"] = response != "n"
|
|
65
|
+
|
|
66
|
+
# Warn suspicious
|
|
67
|
+
print("\nWarn about suspicious packages? [Y/n]")
|
|
68
|
+
response = input("> ").strip().lower()
|
|
69
|
+
config["warn_suspicious"] = response != "n"
|
|
70
|
+
|
|
71
|
+
# Offline mode
|
|
72
|
+
print("\nBehavior when API is unreachable? [warn/block/allow] (default: warn)")
|
|
73
|
+
response = input("> ").strip().lower()
|
|
74
|
+
if response in ("warn", "block", "allow"):
|
|
75
|
+
config["offline_mode"] = response
|
|
76
|
+
|
|
77
|
+
save_config(config)
|
|
78
|
+
print(f"\nConfig saved to {CONFIG_FILE}")
|
|
79
|
+
print("You can now use: stillrunning-pip install <package>\n")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stillrunning-pip
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Secure pip wrapper with supply chain attack protection
|
|
5
|
+
Author-email: "stillrunning.io" <hello@stillrunning.io>
|
|
6
|
+
Project-URL: Homepage, https://stillrunning.io
|
|
7
|
+
Project-URL: Documentation, https://stillrunning.io/docs
|
|
8
|
+
Project-URL: Repository, https://github.com/johhnyg/stillrunning-pip
|
|
9
|
+
Project-URL: Issues, https://github.com/johhnyg/stillrunning-pip/issues
|
|
10
|
+
Keywords: security,supply-chain,pip,malware,protection
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# stillrunning-pip
|
|
28
|
+
|
|
29
|
+
Secure pip wrapper that scans packages for supply chain attacks before installing.
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/stillrunning-pip/)
|
|
32
|
+
[](https://stillrunning.io)
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install stillrunning-pip
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
Use `stillrunning-pip` instead of `pip`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
stillrunning-pip install requests flask
|
|
46
|
+
stillrunning-pip install -r requirements.txt
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or create an alias:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Add to ~/.bashrc or ~/.zshrc
|
|
53
|
+
alias pip='stillrunning-pip'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Setup
|
|
57
|
+
|
|
58
|
+
Configure your token and preferences:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
stillrunning-pip --setup
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or create `~/.stillrunning/config.json` manually:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"token": "sr_your_token_here",
|
|
69
|
+
"block_dangerous": true,
|
|
70
|
+
"warn_suspicious": true,
|
|
71
|
+
"offline_mode": "warn"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Example Output
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
🛡️ stillrunning security scan
|
|
79
|
+
Checking 5 package(s)...
|
|
80
|
+
|
|
81
|
+
✅ CLEAN requests==2.31.0
|
|
82
|
+
✅ CLEAN flask==2.3.0
|
|
83
|
+
⚠️ WARNING sketchy-lib==1.0.0
|
|
84
|
+
→ Obfuscated code patterns detected
|
|
85
|
+
🚫 BLOCKED evil-pkg==0.1.0
|
|
86
|
+
→ Known malicious package (reverse shell)
|
|
87
|
+
|
|
88
|
+
❌ Installation blocked
|
|
89
|
+
1 dangerous package(s) detected
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Configuration Options
|
|
93
|
+
|
|
94
|
+
| Option | Default | Description |
|
|
95
|
+
|--------|---------|-------------|
|
|
96
|
+
| `token` | `""` | stillrunning.io API token for AI scanning |
|
|
97
|
+
| `block_dangerous` | `true` | Block installs for dangerous packages |
|
|
98
|
+
| `warn_suspicious` | `true` | Show warnings for suspicious packages |
|
|
99
|
+
| `offline_mode` | `"warn"` | Behavior when API unreachable: `warn`, `block`, `allow` |
|
|
100
|
+
| `timeout` | `30` | API timeout in seconds |
|
|
101
|
+
|
|
102
|
+
## Environment Variables
|
|
103
|
+
|
|
104
|
+
- `STILLRUNNING_TOKEN` — Override token from config
|
|
105
|
+
|
|
106
|
+
## Free vs Paid
|
|
107
|
+
|
|
108
|
+
| Feature | Free | With Token |
|
|
109
|
+
|---------|------|------------|
|
|
110
|
+
| Known malicious packages | Blocked | Blocked |
|
|
111
|
+
| Threat feed database | Checked | Checked |
|
|
112
|
+
| AI analysis of unknown packages | - | Yes |
|
|
113
|
+
| Scans per day | Unlimited (cached) | 100-10000 |
|
|
114
|
+
|
|
115
|
+
Get a token at [stillrunning.io/pricing](https://stillrunning.io/pricing)
|
|
116
|
+
|
|
117
|
+
## What It Detects
|
|
118
|
+
|
|
119
|
+
- **Known malicious packages** — Packages in our threat database (DPRK campaigns, typosquats, backdoors)
|
|
120
|
+
- **Typosquatting** — Packages with names similar to popular packages
|
|
121
|
+
- **AI-flagged packages** — Obfuscated code, credential harvesting, reverse shells
|
|
122
|
+
|
|
123
|
+
## Bypass (Not Recommended)
|
|
124
|
+
|
|
125
|
+
To bypass scanning for a single install:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install <package> # Use pip directly
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Uninstall
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip uninstall stillrunning-pip
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
stillrunning_pip/__init__.py
|
|
4
|
+
stillrunning_pip/cli.py
|
|
5
|
+
stillrunning_pip/config.py
|
|
6
|
+
stillrunning_pip.egg-info/PKG-INFO
|
|
7
|
+
stillrunning_pip.egg-info/SOURCES.txt
|
|
8
|
+
stillrunning_pip.egg-info/dependency_links.txt
|
|
9
|
+
stillrunning_pip.egg-info/entry_points.txt
|
|
10
|
+
stillrunning_pip.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stillrunning_pip
|