shieldops-cli 1.0.0__tar.gz → 1.0.2__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.
- {shieldops_cli-1.0.0/shieldops_cli.egg-info → shieldops_cli-1.0.2}/PKG-INFO +26 -15
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/README.md +22 -10
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/pyproject.toml +4 -5
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/__init__.py +1 -1
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/auth.py +1 -1
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/analyze.py +42 -9
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/formatters/sarif.py +1 -1
- shieldops_cli-1.0.2/shieldops_cli/local_analyzer.py +218 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2/shieldops_cli.egg-info}/PKG-INFO +26 -15
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli.egg-info/SOURCES.txt +1 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/tests/test_analyze.py +4 -4
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/LICENSE +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/setup.cfg +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/api_client.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/__init__.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/autofix.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/compose_gen.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/compose_scan.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/config_cmd.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/k8s_scan.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/sbom.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/scan_image.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/commands/tui.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/config.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/formatters/__init__.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/formatters/json_fmt.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/formatters/summary.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/formatters/table.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli/main.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli.egg-info/dependency_links.txt +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli.egg-info/entry_points.txt +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli.egg-info/requires.txt +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/shieldops_cli.egg-info/top_level.txt +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/tests/test_auth.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/tests/test_formatters.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/tests/test_phase2_validation.py +0 -0
- {shieldops_cli-1.0.0 → shieldops_cli-1.0.2}/tests/test_score_zero_bug.py +0 -0
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shieldops-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: ShieldOps AI — Security scanner CLI for Docker, Kubernetes, Compose, SBOM, and more.
|
|
5
5
|
Author-email: ShieldOps AI <support@shieldops.ai>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://shieldops-ai.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://shieldops-ai.dev
|
|
8
8
|
Project-URL: Documentation, https://github.com/mohammedabdallahcv-creator/shieldops-cli
|
|
9
9
|
Project-URL: Repository, https://github.com/mohammedabdallahcv-creator/shieldops-cli
|
|
10
|
-
Project-URL: Changelog, https://github.com/mohammedabdallahcv-creator/shieldops-cli/
|
|
10
|
+
Project-URL: Changelog, https://github.com/mohammedabdallahcv-creator/shieldops-cli/blob/main/CHANGELOG.md
|
|
11
11
|
Keywords: docker,kubernetes,security,devsecops,sbom,cli
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Topic :: Security
|
|
16
16
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
18
17
|
Classifier: Programming Language :: Python :: 3
|
|
19
18
|
Requires-Python: >=3.9
|
|
20
19
|
Description-Content-Type: text/markdown
|
|
@@ -38,10 +37,14 @@ Dynamic: license-file
|
|
|
38
37
|
[](https://pypi.org/project/shieldops-cli/)
|
|
39
38
|
[](LICENSE)
|
|
40
39
|
[](https://github.com/mohammedabdallahcv-creator/shieldops-cli)
|
|
41
|
-
[](https://shieldops-ai.
|
|
40
|
+
[](https://shieldops-ai.dev)
|
|
42
41
|
|
|
43
42
|
<p align="center">
|
|
44
|
-
<img src="docs/screenshots/
|
|
43
|
+
<img src="docs/screenshots/tui-session.svg" alt="ShieldOps TUI interactive session" width="800">
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="docs/screenshots/cli-output.svg" alt="ShieldOps CLI scan results" width="800">
|
|
45
48
|
</p>
|
|
46
49
|
|
|
47
50
|
---
|
|
@@ -61,7 +64,8 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
61
64
|
| Docker image scan | Yes | No | Yes (built-in) |
|
|
62
65
|
| Interactive TUI | Yes | No | No |
|
|
63
66
|
| CI/CD ready (`--fail-on`) | Yes | Yes | Yes |
|
|
64
|
-
| Free tier |
|
|
67
|
+
| Free tier (local) | Unlimited scans, no signup | Yes | Yes |
|
|
68
|
+
| Cloud AI analysis | With API key (5 free/day) | — | — |
|
|
65
69
|
|
|
66
70
|
### What makes it different
|
|
67
71
|
|
|
@@ -78,14 +82,21 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
78
82
|
# 1. Install
|
|
79
83
|
pip install shieldops-cli
|
|
80
84
|
|
|
81
|
-
# 2.
|
|
82
|
-
shieldops login
|
|
83
|
-
|
|
84
|
-
# 3. Scan your Dockerfile
|
|
85
|
+
# 2. Scan your Dockerfile (local — no login needed)
|
|
85
86
|
shieldops analyze Dockerfile
|
|
86
87
|
```
|
|
87
88
|
|
|
88
|
-
That's it. You get severity-graded findings
|
|
89
|
+
That's it. You get severity-graded findings with 10+ built-in rules — no signup, no API key.
|
|
90
|
+
|
|
91
|
+
For AI-powered analysis with deeper scanning:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# 3. Login (free tier — 5 scans/day)
|
|
95
|
+
shieldops login
|
|
96
|
+
|
|
97
|
+
# 4. Scan with cloud AI
|
|
98
|
+
shieldops analyze Dockerfile --api
|
|
99
|
+
```
|
|
89
100
|
|
|
90
101
|
---
|
|
91
102
|
|
|
@@ -293,7 +304,7 @@ shieldops-scan:
|
|
|
293
304
|
| Policy engine | No | Yes |
|
|
294
305
|
| Priority queue | No | Yes |
|
|
295
306
|
|
|
296
|
-
Get your API key at [shieldops-ai.
|
|
307
|
+
Get your API key at [shieldops-ai.dev](https://shieldops-ai.dev).
|
|
297
308
|
|
|
298
309
|
---
|
|
299
310
|
|
|
@@ -348,4 +359,4 @@ MIT
|
|
|
348
359
|
|
|
349
360
|
---
|
|
350
361
|
|
|
351
|
-
ShieldOps CLI is open-source. The analysis backend is proprietary and hosted at [shieldops-ai.
|
|
362
|
+
ShieldOps CLI is open-source. The analysis backend is proprietary and hosted at [shieldops-ai.dev](https://shieldops-ai.dev).
|
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
[](https://pypi.org/project/shieldops-cli/)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](https://github.com/mohammedabdallahcv-creator/shieldops-cli)
|
|
9
|
-
[](https://shieldops-ai.
|
|
9
|
+
[](https://shieldops-ai.dev)
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img src="docs/screenshots/
|
|
12
|
+
<img src="docs/screenshots/tui-session.svg" alt="ShieldOps TUI interactive session" width="800">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="docs/screenshots/cli-output.svg" alt="ShieldOps CLI scan results" width="800">
|
|
13
17
|
</p>
|
|
14
18
|
|
|
15
19
|
---
|
|
@@ -29,7 +33,8 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
29
33
|
| Docker image scan | Yes | No | Yes (built-in) |
|
|
30
34
|
| Interactive TUI | Yes | No | No |
|
|
31
35
|
| CI/CD ready (`--fail-on`) | Yes | Yes | Yes |
|
|
32
|
-
| Free tier |
|
|
36
|
+
| Free tier (local) | Unlimited scans, no signup | Yes | Yes |
|
|
37
|
+
| Cloud AI analysis | With API key (5 free/day) | — | — |
|
|
33
38
|
|
|
34
39
|
### What makes it different
|
|
35
40
|
|
|
@@ -46,14 +51,21 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
46
51
|
# 1. Install
|
|
47
52
|
pip install shieldops-cli
|
|
48
53
|
|
|
49
|
-
# 2.
|
|
50
|
-
shieldops login
|
|
51
|
-
|
|
52
|
-
# 3. Scan your Dockerfile
|
|
54
|
+
# 2. Scan your Dockerfile (local — no login needed)
|
|
53
55
|
shieldops analyze Dockerfile
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
That's it. You get severity-graded findings
|
|
58
|
+
That's it. You get severity-graded findings with 10+ built-in rules — no signup, no API key.
|
|
59
|
+
|
|
60
|
+
For AI-powered analysis with deeper scanning:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# 3. Login (free tier — 5 scans/day)
|
|
64
|
+
shieldops login
|
|
65
|
+
|
|
66
|
+
# 4. Scan with cloud AI
|
|
67
|
+
shieldops analyze Dockerfile --api
|
|
68
|
+
```
|
|
57
69
|
|
|
58
70
|
---
|
|
59
71
|
|
|
@@ -261,7 +273,7 @@ shieldops-scan:
|
|
|
261
273
|
| Policy engine | No | Yes |
|
|
262
274
|
| Priority queue | No | Yes |
|
|
263
275
|
|
|
264
|
-
Get your API key at [shieldops-ai.
|
|
276
|
+
Get your API key at [shieldops-ai.dev](https://shieldops-ai.dev).
|
|
265
277
|
|
|
266
278
|
---
|
|
267
279
|
|
|
@@ -316,4 +328,4 @@ MIT
|
|
|
316
328
|
|
|
317
329
|
---
|
|
318
330
|
|
|
319
|
-
ShieldOps CLI is open-source. The analysis backend is proprietary and hosted at [shieldops-ai.
|
|
331
|
+
ShieldOps CLI is open-source. The analysis backend is proprietary and hosted at [shieldops-ai.dev](https://shieldops-ai.dev).
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shieldops-cli"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.2"
|
|
8
8
|
description = "ShieldOps AI — Security scanner CLI for Docker, Kubernetes, Compose, SBOM, and more."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
license =
|
|
10
|
+
license = "MIT"
|
|
11
11
|
requires-python = ">=3.9"
|
|
12
12
|
authors = [{name = "ShieldOps AI", email = "support@shieldops.ai"}]
|
|
13
13
|
keywords = ["docker", "kubernetes", "security", "devsecops", "sbom", "cli"]
|
|
@@ -17,7 +17,6 @@ classifiers = [
|
|
|
17
17
|
"Intended Audience :: Developers",
|
|
18
18
|
"Topic :: Security",
|
|
19
19
|
"Topic :: Software Development :: Quality Assurance",
|
|
20
|
-
"License :: OSI Approved :: MIT License",
|
|
21
20
|
"Programming Language :: Python :: 3",
|
|
22
21
|
]
|
|
23
22
|
dependencies = [
|
|
@@ -34,7 +33,7 @@ tui = ["prompt_toolkit>=3.0.43"]
|
|
|
34
33
|
shieldops = "shieldops_cli.main:cli"
|
|
35
34
|
|
|
36
35
|
[project.urls]
|
|
37
|
-
Homepage = "https://shieldops-ai.
|
|
36
|
+
Homepage = "https://shieldops-ai.dev"
|
|
38
37
|
Documentation = "https://github.com/mohammedabdallahcv-creator/shieldops-cli"
|
|
39
38
|
Repository = "https://github.com/mohammedabdallahcv-creator/shieldops-cli"
|
|
40
|
-
Changelog = "https://github.com/mohammedabdallahcv-creator/shieldops-cli/
|
|
39
|
+
Changelog = "https://github.com/mohammedabdallahcv-creator/shieldops-cli/blob/main/CHANGELOG.md"
|
|
@@ -9,7 +9,7 @@ console = Console()
|
|
|
9
9
|
|
|
10
10
|
@click.command()
|
|
11
11
|
@click.option("--key", prompt="API Key", hide_input=True,
|
|
12
|
-
help="API key from https://shieldops-ai.
|
|
12
|
+
help="API key from https://shieldops-ai.dev/settings/api-keys")
|
|
13
13
|
@click.option("--url", default=None, help="Override API base URL.")
|
|
14
14
|
def login(key, url):
|
|
15
15
|
"""Authenticate with your ShieldOps API key."""
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
"""shieldops analyze — Dockerfile analysis."""
|
|
1
|
+
"""shieldops analyze — Dockerfile analysis (local or cloud)."""
|
|
2
2
|
import sys
|
|
3
3
|
import click
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
|
|
7
|
+
from shieldops_cli import config as cfg
|
|
7
8
|
from shieldops_cli.api_client import ShieldOpsClient, ApiError
|
|
8
9
|
from shieldops_cli.formatters import format_result
|
|
10
|
+
from shieldops_cli.local_analyzer import analyze_dockerfile as local_analyze
|
|
9
11
|
|
|
10
12
|
console = Console()
|
|
11
13
|
|
|
@@ -19,23 +21,58 @@ SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
|
19
21
|
@click.option("-o", "--output", type=click.Path(), default=None,
|
|
20
22
|
help="Write output to file instead of stdout.")
|
|
21
23
|
@click.option("--open-report", is_flag=True, default=False,
|
|
22
|
-
help="Open the full report in browser after scan.")
|
|
24
|
+
help="Open the full report in browser after scan (cloud only).")
|
|
23
25
|
@click.option("--fail-on", type=click.Choice(["critical", "high", "medium", "low", "none"]),
|
|
24
26
|
default="none", help="Exit with code 1 if issues >= severity (for CI/CD).")
|
|
25
|
-
|
|
27
|
+
@click.option("--api", "force_api", is_flag=True, default=False,
|
|
28
|
+
help="Force cloud analysis (requires login).")
|
|
29
|
+
def analyze(file, fmt, output, open_report, fail_on, force_api):
|
|
26
30
|
"""Analyze a Dockerfile for security and best-practice issues.
|
|
27
31
|
|
|
32
|
+
Runs locally by default (no API key needed). Use --api for cloud analysis.
|
|
33
|
+
|
|
28
34
|
\b
|
|
29
35
|
Examples:
|
|
30
36
|
shieldops analyze Dockerfile
|
|
37
|
+
shieldops analyze Dockerfile --api # cloud (requires login)
|
|
31
38
|
shieldops analyze Dockerfile --format json --output report.json
|
|
32
|
-
shieldops analyze Dockerfile --fail-on high
|
|
33
|
-
shieldops analyze Dockerfile --open-report
|
|
39
|
+
shieldops analyze Dockerfile --fail-on high # CI/CD gate
|
|
34
40
|
"""
|
|
35
41
|
path = Path(file)
|
|
36
42
|
content = path.read_text(encoding="utf-8")
|
|
37
43
|
filename = path.name
|
|
38
44
|
|
|
45
|
+
api_key = cfg.get_api_key()
|
|
46
|
+
|
|
47
|
+
if api_key or force_api:
|
|
48
|
+
if not api_key:
|
|
49
|
+
console.print("[red]Cloud analysis requires login. Run: shieldops login --key <YOUR_KEY>[/red]")
|
|
50
|
+
sys.exit(2)
|
|
51
|
+
_run_cloud_analysis(content, filename, fmt, output, open_report, fail_on)
|
|
52
|
+
else:
|
|
53
|
+
_run_local_analysis(content, filename, fmt, output, fail_on)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_local_analysis(content: str, filename: str, fmt, output, fail_on):
|
|
57
|
+
result = local_analyze(content, filename)
|
|
58
|
+
formatted = format_result("analyze", result, fmt=fmt or "table")
|
|
59
|
+
|
|
60
|
+
if output:
|
|
61
|
+
Path(output).write_text(formatted, encoding="utf-8")
|
|
62
|
+
console.print(f"[green]\u2705 Report saved to {output}[/green]")
|
|
63
|
+
else:
|
|
64
|
+
console.print(formatted)
|
|
65
|
+
|
|
66
|
+
console.print("\n[dim][Local analysis - 10 rules] Sign up for cloud analysis with AI:[/dim]")
|
|
67
|
+
console.print(f"[dim] {cfg.get_api_url()}/settings/api-keys[/dim]")
|
|
68
|
+
|
|
69
|
+
if fail_on != "none":
|
|
70
|
+
if check_severity_gate(result, fail_on):
|
|
71
|
+
console.print(f"\n[red]\u274c Issues found at {fail_on.upper()} or above. Failing.[/red]")
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _run_cloud_analysis(content: str, filename: str, fmt, output, open_report, fail_on):
|
|
39
76
|
client = ShieldOpsClient()
|
|
40
77
|
|
|
41
78
|
with console.status("[bold blue]Analyzing...", spinner="dots"):
|
|
@@ -47,7 +84,6 @@ def analyze(file, fmt, output, open_report, fail_on):
|
|
|
47
84
|
|
|
48
85
|
result = payload.get("result", {})
|
|
49
86
|
|
|
50
|
-
# ── Format output ──
|
|
51
87
|
formatted = format_result("analyze", result, fmt=fmt or "table")
|
|
52
88
|
|
|
53
89
|
if output:
|
|
@@ -56,7 +92,6 @@ def analyze(file, fmt, output, open_report, fail_on):
|
|
|
56
92
|
else:
|
|
57
93
|
console.print(formatted)
|
|
58
94
|
|
|
59
|
-
# ── Report URL (canonical: result.report_url → top-level route → scan_id fallback) ──
|
|
60
95
|
report_url = (
|
|
61
96
|
result.get("report_url")
|
|
62
97
|
or payload.get("route")
|
|
@@ -70,14 +105,12 @@ def analyze(file, fmt, output, open_report, fail_on):
|
|
|
70
105
|
base = client.api_url
|
|
71
106
|
full_url = report_url if report_url.startswith("http") else f"{base}{report_url}"
|
|
72
107
|
print(f"\nFull report: {full_url}")
|
|
73
|
-
|
|
74
108
|
if open_report:
|
|
75
109
|
import webbrowser
|
|
76
110
|
webbrowser.open(full_url)
|
|
77
111
|
else:
|
|
78
112
|
print("\nNo report URL returned for this scan.")
|
|
79
113
|
|
|
80
|
-
# ── CI/CD exit code ──
|
|
81
114
|
if fail_on != "none":
|
|
82
115
|
if check_severity_gate(result, fail_on):
|
|
83
116
|
console.print(f"\n[red]\u274c Issues found at {fail_on.upper()} or above. Failing.[/red]")
|
|
@@ -50,7 +50,7 @@ def to_sarif(task: str, result: dict) -> str:
|
|
|
50
50
|
"tool": {
|
|
51
51
|
"driver": {
|
|
52
52
|
"name": "ShieldOps AI",
|
|
53
|
-
"informationUri": "https://shieldops-ai.
|
|
53
|
+
"informationUri": "https://shieldops-ai.dev",
|
|
54
54
|
"version": "1.0.0",
|
|
55
55
|
"rules": list(rules.values()),
|
|
56
56
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Local Dockerfile analyzer — works offline, no API key needed."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
RULES: list[dict] = [
|
|
7
|
+
# ── DL3007: latest tag ──
|
|
8
|
+
{
|
|
9
|
+
"rule_id": "DL3007",
|
|
10
|
+
"severity": "critical",
|
|
11
|
+
"pattern": re.compile(r"^FROM\s+\S+?:\s*latest\s*$", re.IGNORECASE | re.MULTILINE),
|
|
12
|
+
"message": "Using `latest` tag is dangerous — pinned versions ensure reproducible builds",
|
|
13
|
+
"fix": "Replace `latest` with a specific version tag (e.g. `python:3.11-slim`)",
|
|
14
|
+
"category": "best_practice",
|
|
15
|
+
},
|
|
16
|
+
# ── DL3008: pin apt-get versions ──
|
|
17
|
+
{
|
|
18
|
+
"rule_id": "DL3008",
|
|
19
|
+
"severity": "critical",
|
|
20
|
+
"pattern": re.compile(
|
|
21
|
+
r"RUN\s+(apt-get\s+install\s+)(?!.*\s=\s)",
|
|
22
|
+
re.IGNORECASE | re.MULTILINE,
|
|
23
|
+
),
|
|
24
|
+
"message": "Pin package versions in `apt-get install` for reproducible builds",
|
|
25
|
+
"fix": "Add `=version` to each package (e.g. `build-essential=12.9`)",
|
|
26
|
+
"category": "best_practice",
|
|
27
|
+
},
|
|
28
|
+
# ── DL3009: apt-get lists not cleaned ──
|
|
29
|
+
{
|
|
30
|
+
"rule_id": "DL3009",
|
|
31
|
+
"severity": "medium",
|
|
32
|
+
"pattern": re.compile(
|
|
33
|
+
r"RUN\s+apt-get\s+install",
|
|
34
|
+
re.IGNORECASE,
|
|
35
|
+
),
|
|
36
|
+
"message": "Delete apt-get lists after install to reduce image size",
|
|
37
|
+
"fix": "Add `&& rm -rf /var/lib/apt/lists/*` after apt-get install",
|
|
38
|
+
"category": "best_practice",
|
|
39
|
+
"_needs_clean": True,
|
|
40
|
+
},
|
|
41
|
+
# ── DL3013: pin pip versions ──
|
|
42
|
+
{
|
|
43
|
+
"rule_id": "DL3013",
|
|
44
|
+
"severity": "high",
|
|
45
|
+
"pattern": re.compile(
|
|
46
|
+
r"RUN\s+pip\s+install\s+(?!.*==)",
|
|
47
|
+
re.IGNORECASE | re.MULTILINE,
|
|
48
|
+
),
|
|
49
|
+
"message": "Pin package versions in pip install for reproducible builds",
|
|
50
|
+
"fix": "Use `pip install flask==3.0.0` instead of `pip install flask`",
|
|
51
|
+
"category": "best_practice",
|
|
52
|
+
},
|
|
53
|
+
# ── DL4006: SHELL selected by default ──
|
|
54
|
+
{
|
|
55
|
+
"rule_id": "DL4006",
|
|
56
|
+
"severity": "high",
|
|
57
|
+
"pattern": re.compile(r"^SHELL\s+\[", re.IGNORECASE | re.MULTILINE),
|
|
58
|
+
"message": "SHELL is already selected by default — remove redundant SHELL directive",
|
|
59
|
+
"fix": "Remove the SHELL directive unless you need a non-default shell",
|
|
60
|
+
"category": "style",
|
|
61
|
+
},
|
|
62
|
+
# ── DL4001: Windows line endings ──
|
|
63
|
+
{
|
|
64
|
+
"rule_id": "DL4001",
|
|
65
|
+
"severity": "low",
|
|
66
|
+
"pattern": re.compile(r"\r\n"),
|
|
67
|
+
"message": "Windows-style line endings detected — use LF for Dockerfiles",
|
|
68
|
+
"fix": "Convert to Unix line endings (LF)",
|
|
69
|
+
"category": "style",
|
|
70
|
+
},
|
|
71
|
+
# ── DL3003: no WORKDIR before COPY ──
|
|
72
|
+
{
|
|
73
|
+
"rule_id": "DL3003",
|
|
74
|
+
"severity": "low",
|
|
75
|
+
"pattern": re.compile(r"^COPY\s+\.\s+/"),
|
|
76
|
+
"message": "COPY without prior WORKDIR — files may land in unexpected location",
|
|
77
|
+
"fix": "Add `WORKDIR /app` before COPY",
|
|
78
|
+
"category": "best_practice",
|
|
79
|
+
},
|
|
80
|
+
# ── USER root ──
|
|
81
|
+
{
|
|
82
|
+
"rule_id": "SC1001",
|
|
83
|
+
"severity": "critical",
|
|
84
|
+
"pattern": re.compile(r"^USER\s+root\b", re.IGNORECASE | re.MULTILINE),
|
|
85
|
+
"message": "Running as root increases blast radius in case of container escape",
|
|
86
|
+
"fix": "Use `USER nonroot` or create a dedicated user with `RUN adduser -D appuser && USER appuser`",
|
|
87
|
+
"category": "security",
|
|
88
|
+
},
|
|
89
|
+
# ── EXPOSE without port number ──
|
|
90
|
+
{
|
|
91
|
+
"rule_id": "DL4000",
|
|
92
|
+
"severity": "low",
|
|
93
|
+
"pattern": re.compile(r"^EXPOSE\s*$", re.MULTILINE),
|
|
94
|
+
"message": "EXPOSE without port number has no effect",
|
|
95
|
+
"fix": "Specify a port: `EXPOSE 8080`",
|
|
96
|
+
"category": "best_practice",
|
|
97
|
+
},
|
|
98
|
+
# ── ENV with debug mode in production ──
|
|
99
|
+
{
|
|
100
|
+
"rule_id": "SC1002",
|
|
101
|
+
"severity": "medium",
|
|
102
|
+
"pattern": re.compile(
|
|
103
|
+
r"ENV\s+(FLASK_DEBUG|NODE_ENV|DJANGO_DEBUG|APP_DEBUG)\s*=\s*(1|true|development)\b",
|
|
104
|
+
re.IGNORECASE | re.MULTILINE,
|
|
105
|
+
),
|
|
106
|
+
"message": "Debug/development mode enabled in production image",
|
|
107
|
+
"fix": "Set `ENV FLASK_DEBUG=0` or remove the ENV line for production images",
|
|
108
|
+
"category": "security",
|
|
109
|
+
},
|
|
110
|
+
# ── No HEALTHCHECK ──
|
|
111
|
+
{
|
|
112
|
+
"rule_id": "DL4002",
|
|
113
|
+
"severity": "low",
|
|
114
|
+
"pattern": re.compile(r"^(?!.*HEALTHCHECK)", re.MULTILINE),
|
|
115
|
+
"message": "No HEALTHCHECK defined — container health won't be monitored",
|
|
116
|
+
"fix": "Add `HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1`",
|
|
117
|
+
"category": "best_practice",
|
|
118
|
+
"_negate": True,
|
|
119
|
+
},
|
|
120
|
+
# ── RUN npm install without cache cleanup ──
|
|
121
|
+
{
|
|
122
|
+
"rule_id": "DL3014",
|
|
123
|
+
"severity": "medium",
|
|
124
|
+
"pattern": re.compile(
|
|
125
|
+
r"RUN\s+npm\s+install\s+(?!.*npm\s+cache\s+clean)",
|
|
126
|
+
re.IGNORECASE | re.MULTILINE,
|
|
127
|
+
),
|
|
128
|
+
"message": "npm install without cache cleanup increases image size",
|
|
129
|
+
"fix": "Add `&& npm cache clean --force` to the RUN command",
|
|
130
|
+
"category": "best_practice",
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
FINDING_TEMPLATE = {
|
|
135
|
+
"type": "dockerfile",
|
|
136
|
+
"category": "",
|
|
137
|
+
"severity": "info",
|
|
138
|
+
"rule_id": "",
|
|
139
|
+
"message": "",
|
|
140
|
+
"fix": "",
|
|
141
|
+
"line": 1,
|
|
142
|
+
"column": 1,
|
|
143
|
+
"source": "shieldops-local",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def analyze_dockerfile(content: str, filename: str = "Dockerfile") -> dict:
|
|
148
|
+
lines = content.split("\n")
|
|
149
|
+
findings = []
|
|
150
|
+
|
|
151
|
+
for rule in RULES:
|
|
152
|
+
pattern = rule["pattern"]
|
|
153
|
+
negate = rule.get("_negate", False)
|
|
154
|
+
|
|
155
|
+
if negate:
|
|
156
|
+
if not pattern.search(content):
|
|
157
|
+
finding = dict(FINDING_TEMPLATE)
|
|
158
|
+
finding.update(
|
|
159
|
+
severity=rule["severity"],
|
|
160
|
+
rule_id=rule["rule_id"],
|
|
161
|
+
message=rule["message"],
|
|
162
|
+
fix=rule["fix"],
|
|
163
|
+
category=rule["category"],
|
|
164
|
+
line=1,
|
|
165
|
+
)
|
|
166
|
+
findings.append(finding)
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
for match in pattern.finditer(content):
|
|
170
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
171
|
+
raw_line = lines[line_num - 1] if line_num <= len(lines) else ""
|
|
172
|
+
|
|
173
|
+
if rule.get("_needs_clean"):
|
|
174
|
+
block = content[match.start() : min(match.start() + 300, len(content))]
|
|
175
|
+
if "rm -rf" in block and "apt/lists" in block:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
if rule["rule_id"] == "DL3013":
|
|
179
|
+
rest_of_line = raw_line[match.end() - len(raw_line) :]
|
|
180
|
+
if "==" in rest_of_line or "requirements.txt" in rest_of_line or "-r" in rest_of_line:
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
if rule["rule_id"] == "DL3008":
|
|
184
|
+
rest = raw_line[match.end() - len(raw_line) :]
|
|
185
|
+
if "=" in rest:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
finding = dict(FINDING_TEMPLATE)
|
|
189
|
+
finding.update(
|
|
190
|
+
severity=rule["severity"],
|
|
191
|
+
rule_id=rule["rule_id"],
|
|
192
|
+
message=rule["message"],
|
|
193
|
+
fix=rule["fix"],
|
|
194
|
+
category=rule["category"],
|
|
195
|
+
line=line_num,
|
|
196
|
+
)
|
|
197
|
+
findings.append(finding)
|
|
198
|
+
|
|
199
|
+
critical = sum(1 for f in findings if f["severity"] == "critical")
|
|
200
|
+
high = sum(1 for f in findings if f["severity"] == "high")
|
|
201
|
+
medium = sum(1 for f in findings if f["severity"] == "medium")
|
|
202
|
+
low = sum(1 for f in findings if f["severity"] == "low")
|
|
203
|
+
|
|
204
|
+
score = max(0, 100 - (critical * 25 + high * 10 + medium * 5 + low * 2))
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"findings": findings,
|
|
208
|
+
"report_contract": {
|
|
209
|
+
"critical_count": critical,
|
|
210
|
+
"high_count": high,
|
|
211
|
+
"medium_count": medium,
|
|
212
|
+
"low_count": low,
|
|
213
|
+
},
|
|
214
|
+
"security_score": score,
|
|
215
|
+
"security_score_grade": "A" if score >= 90 else "B" if score >= 70 else "C" if score >= 50 else "D" if score >= 30 else "F",
|
|
216
|
+
"source": "local",
|
|
217
|
+
"filename": filename,
|
|
218
|
+
}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shieldops-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: ShieldOps AI — Security scanner CLI for Docker, Kubernetes, Compose, SBOM, and more.
|
|
5
5
|
Author-email: ShieldOps AI <support@shieldops.ai>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://shieldops-ai.
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://shieldops-ai.dev
|
|
8
8
|
Project-URL: Documentation, https://github.com/mohammedabdallahcv-creator/shieldops-cli
|
|
9
9
|
Project-URL: Repository, https://github.com/mohammedabdallahcv-creator/shieldops-cli
|
|
10
|
-
Project-URL: Changelog, https://github.com/mohammedabdallahcv-creator/shieldops-cli/
|
|
10
|
+
Project-URL: Changelog, https://github.com/mohammedabdallahcv-creator/shieldops-cli/blob/main/CHANGELOG.md
|
|
11
11
|
Keywords: docker,kubernetes,security,devsecops,sbom,cli
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Topic :: Security
|
|
16
16
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
18
17
|
Classifier: Programming Language :: Python :: 3
|
|
19
18
|
Requires-Python: >=3.9
|
|
20
19
|
Description-Content-Type: text/markdown
|
|
@@ -38,10 +37,14 @@ Dynamic: license-file
|
|
|
38
37
|
[](https://pypi.org/project/shieldops-cli/)
|
|
39
38
|
[](LICENSE)
|
|
40
39
|
[](https://github.com/mohammedabdallahcv-creator/shieldops-cli)
|
|
41
|
-
[](https://shieldops-ai.
|
|
40
|
+
[](https://shieldops-ai.dev)
|
|
42
41
|
|
|
43
42
|
<p align="center">
|
|
44
|
-
<img src="docs/screenshots/
|
|
43
|
+
<img src="docs/screenshots/tui-session.svg" alt="ShieldOps TUI interactive session" width="800">
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<p align="center">
|
|
47
|
+
<img src="docs/screenshots/cli-output.svg" alt="ShieldOps CLI scan results" width="800">
|
|
45
48
|
</p>
|
|
46
49
|
|
|
47
50
|
---
|
|
@@ -61,7 +64,8 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
61
64
|
| Docker image scan | Yes | No | Yes (built-in) |
|
|
62
65
|
| Interactive TUI | Yes | No | No |
|
|
63
66
|
| CI/CD ready (`--fail-on`) | Yes | Yes | Yes |
|
|
64
|
-
| Free tier |
|
|
67
|
+
| Free tier (local) | Unlimited scans, no signup | Yes | Yes |
|
|
68
|
+
| Cloud AI analysis | With API key (5 free/day) | — | — |
|
|
65
69
|
|
|
66
70
|
### What makes it different
|
|
67
71
|
|
|
@@ -78,14 +82,21 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
78
82
|
# 1. Install
|
|
79
83
|
pip install shieldops-cli
|
|
80
84
|
|
|
81
|
-
# 2.
|
|
82
|
-
shieldops login
|
|
83
|
-
|
|
84
|
-
# 3. Scan your Dockerfile
|
|
85
|
+
# 2. Scan your Dockerfile (local — no login needed)
|
|
85
86
|
shieldops analyze Dockerfile
|
|
86
87
|
```
|
|
87
88
|
|
|
88
|
-
That's it. You get severity-graded findings
|
|
89
|
+
That's it. You get severity-graded findings with 10+ built-in rules — no signup, no API key.
|
|
90
|
+
|
|
91
|
+
For AI-powered analysis with deeper scanning:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# 3. Login (free tier — 5 scans/day)
|
|
95
|
+
shieldops login
|
|
96
|
+
|
|
97
|
+
# 4. Scan with cloud AI
|
|
98
|
+
shieldops analyze Dockerfile --api
|
|
99
|
+
```
|
|
89
100
|
|
|
90
101
|
---
|
|
91
102
|
|
|
@@ -293,7 +304,7 @@ shieldops-scan:
|
|
|
293
304
|
| Policy engine | No | Yes |
|
|
294
305
|
| Priority queue | No | Yes |
|
|
295
306
|
|
|
296
|
-
Get your API key at [shieldops-ai.
|
|
307
|
+
Get your API key at [shieldops-ai.dev](https://shieldops-ai.dev).
|
|
297
308
|
|
|
298
309
|
---
|
|
299
310
|
|
|
@@ -348,4 +359,4 @@ MIT
|
|
|
348
359
|
|
|
349
360
|
---
|
|
350
361
|
|
|
351
|
-
ShieldOps CLI is open-source. The analysis backend is proprietary and hosted at [shieldops-ai.
|
|
362
|
+
ShieldOps CLI is open-source. The analysis backend is proprietary and hosted at [shieldops-ai.dev](https://shieldops-ai.dev).
|
|
@@ -35,7 +35,7 @@ def test_analyze_table_output(runner, mock_config, sample_dockerfile):
|
|
|
35
35
|
with patch("shieldops_cli.commands.analyze.ShieldOpsClient") as mock_cls:
|
|
36
36
|
mock_client = MagicMock()
|
|
37
37
|
mock_client.run_task.return_value = MOCK_ANALYZE_RESPONSE
|
|
38
|
-
mock_client.api_url = "https://shieldops-ai.
|
|
38
|
+
mock_client.api_url = "https://shieldops-ai.dev"
|
|
39
39
|
mock_cls.return_value = mock_client
|
|
40
40
|
|
|
41
41
|
result = runner.invoke(cli, ["analyze", str(sample_dockerfile)])
|
|
@@ -50,7 +50,7 @@ def test_analyze_json_output(runner, mock_config, sample_dockerfile):
|
|
|
50
50
|
with patch("shieldops_cli.commands.analyze.ShieldOpsClient") as mock_cls:
|
|
51
51
|
mock_client = MagicMock()
|
|
52
52
|
mock_client.run_task.return_value = MOCK_ANALYZE_RESPONSE
|
|
53
|
-
mock_client.api_url = "https://shieldops-ai.
|
|
53
|
+
mock_client.api_url = "https://shieldops-ai.dev"
|
|
54
54
|
mock_cls.return_value = mock_client
|
|
55
55
|
|
|
56
56
|
result = runner.invoke(cli, ["analyze", str(sample_dockerfile), "--format", "json"])
|
|
@@ -65,7 +65,7 @@ def test_analyze_fail_on_high(runner, mock_config, sample_dockerfile):
|
|
|
65
65
|
with patch("shieldops_cli.commands.analyze.ShieldOpsClient") as mock_cls:
|
|
66
66
|
mock_client = MagicMock()
|
|
67
67
|
mock_client.run_task.return_value = MOCK_ANALYZE_RESPONSE
|
|
68
|
-
mock_client.api_url = "https://shieldops-ai.
|
|
68
|
+
mock_client.api_url = "https://shieldops-ai.dev"
|
|
69
69
|
mock_cls.return_value = mock_client
|
|
70
70
|
|
|
71
71
|
result = runner.invoke(cli, ["analyze", str(sample_dockerfile), "--fail-on", "high"])
|
|
@@ -82,7 +82,7 @@ def test_analyze_save_to_file(runner, mock_config, sample_dockerfile, tmp_path):
|
|
|
82
82
|
with patch("shieldops_cli.commands.analyze.ShieldOpsClient") as mock_cls:
|
|
83
83
|
mock_client = MagicMock()
|
|
84
84
|
mock_client.run_task.return_value = MOCK_ANALYZE_RESPONSE
|
|
85
|
-
mock_client.api_url = "https://shieldops-ai.
|
|
85
|
+
mock_client.api_url = "https://shieldops-ai.dev"
|
|
86
86
|
mock_cls.return_value = mock_client
|
|
87
87
|
|
|
88
88
|
result = runner.invoke(cli, ["analyze", str(sample_dockerfile), "-f", "json", "-o", str(output_file)])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|