shieldops-cli 1.0.1__tar.gz → 1.0.3__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.1/shieldops_cli.egg-info → shieldops_cli-1.0.3}/PKG-INFO +24 -13
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/README.md +23 -12
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/pyproject.toml +1 -1
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/__init__.py +1 -1
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/analyze.py +42 -9
- shieldops_cli-1.0.3/shieldops_cli/local_analyzer.py +218 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3/shieldops_cli.egg-info}/PKG-INFO +24 -13
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli.egg-info/SOURCES.txt +1 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/LICENSE +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/setup.cfg +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/api_client.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/auth.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/__init__.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/autofix.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/compose_gen.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/compose_scan.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/config_cmd.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/k8s_scan.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/sbom.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/scan_image.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/commands/tui.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/config.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/formatters/__init__.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/formatters/json_fmt.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/formatters/sarif.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/formatters/summary.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/formatters/table.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli/main.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli.egg-info/dependency_links.txt +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli.egg-info/entry_points.txt +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli.egg-info/requires.txt +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/shieldops_cli.egg-info/top_level.txt +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/tests/test_analyze.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/tests/test_auth.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/tests/test_formatters.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/tests/test_phase2_validation.py +0 -0
- {shieldops_cli-1.0.1 → shieldops_cli-1.0.3}/tests/test_score_zero_bug.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shieldops-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
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
6
|
License-Expression: MIT
|
|
@@ -40,11 +40,11 @@ Dynamic: license-file
|
|
|
40
40
|
[](https://shieldops-ai.dev)
|
|
41
41
|
|
|
42
42
|
<p align="center">
|
|
43
|
-
<img src="docs/screenshots/tui-session.
|
|
43
|
+
<img src="https://raw.githubusercontent.com/mohammedabdallahcv-creator/shieldops-cli/main/docs/screenshots/tui-session.png" alt="ShieldOps TUI interactive session" width="800">
|
|
44
44
|
</p>
|
|
45
45
|
|
|
46
46
|
<p align="center">
|
|
47
|
-
<img src="docs/screenshots/cli-output.
|
|
47
|
+
<img src="https://raw.githubusercontent.com/mohammedabdallahcv-creator/shieldops-cli/main/docs/screenshots/cli-output.png" alt="ShieldOps CLI scan results" width="800">
|
|
48
48
|
</p>
|
|
49
49
|
|
|
50
50
|
---
|
|
@@ -64,7 +64,8 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
64
64
|
| Docker image scan | Yes | No | Yes (built-in) |
|
|
65
65
|
| Interactive TUI | Yes | No | No |
|
|
66
66
|
| CI/CD ready (`--fail-on`) | Yes | Yes | Yes |
|
|
67
|
-
| Free tier |
|
|
67
|
+
| Free tier (local) | Unlimited scans, no signup | Yes | Yes |
|
|
68
|
+
| Cloud AI analysis | With API key (5 free/day) | — | — |
|
|
68
69
|
|
|
69
70
|
### What makes it different
|
|
70
71
|
|
|
@@ -81,14 +82,21 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
81
82
|
# 1. Install
|
|
82
83
|
pip install shieldops-cli
|
|
83
84
|
|
|
84
|
-
# 2.
|
|
85
|
-
shieldops login
|
|
86
|
-
|
|
87
|
-
# 3. Scan your Dockerfile
|
|
85
|
+
# 2. Scan your Dockerfile (local — no login needed)
|
|
88
86
|
shieldops analyze Dockerfile
|
|
89
87
|
```
|
|
90
88
|
|
|
91
|
-
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
|
+
```
|
|
92
100
|
|
|
93
101
|
---
|
|
94
102
|
|
|
@@ -118,11 +126,14 @@ pip install shieldops-cli
|
|
|
118
126
|
|
|
119
127
|
### `analyze` — Dockerfile Security Scan
|
|
120
128
|
|
|
129
|
+
Runs locally by default (no API key). Use `--api` for cloud AI analysis.
|
|
130
|
+
|
|
121
131
|
```bash
|
|
122
|
-
shieldops analyze Dockerfile
|
|
123
|
-
shieldops analyze Dockerfile --
|
|
124
|
-
shieldops analyze Dockerfile --
|
|
125
|
-
shieldops analyze Dockerfile --
|
|
132
|
+
shieldops analyze Dockerfile # local (free, unlimited)
|
|
133
|
+
shieldops analyze Dockerfile --api # cloud AI (requires login)
|
|
134
|
+
shieldops analyze Dockerfile --format json --output report.json
|
|
135
|
+
shieldops analyze Dockerfile --fail-on high # CI/CD gate
|
|
136
|
+
shieldops analyze Dockerfile --open-report # open browser report
|
|
126
137
|
```
|
|
127
138
|
|
|
128
139
|
### `autofix` — AI-Powered Dockerfile Fix
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
[](https://shieldops-ai.dev)
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img src="docs/screenshots/tui-session.
|
|
12
|
+
<img src="https://raw.githubusercontent.com/mohammedabdallahcv-creator/shieldops-cli/main/docs/screenshots/tui-session.png" alt="ShieldOps TUI interactive session" width="800">
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
<img src="docs/screenshots/cli-output.
|
|
16
|
+
<img src="https://raw.githubusercontent.com/mohammedabdallahcv-creator/shieldops-cli/main/docs/screenshots/cli-output.png" alt="ShieldOps CLI scan results" width="800">
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
---
|
|
@@ -33,7 +33,8 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
33
33
|
| Docker image scan | Yes | No | Yes (built-in) |
|
|
34
34
|
| Interactive TUI | Yes | No | No |
|
|
35
35
|
| CI/CD ready (`--fail-on`) | Yes | Yes | Yes |
|
|
36
|
-
| Free tier |
|
|
36
|
+
| Free tier (local) | Unlimited scans, no signup | Yes | Yes |
|
|
37
|
+
| Cloud AI analysis | With API key (5 free/day) | — | — |
|
|
37
38
|
|
|
38
39
|
### What makes it different
|
|
39
40
|
|
|
@@ -50,14 +51,21 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
50
51
|
# 1. Install
|
|
51
52
|
pip install shieldops-cli
|
|
52
53
|
|
|
53
|
-
# 2.
|
|
54
|
-
shieldops login
|
|
55
|
-
|
|
56
|
-
# 3. Scan your Dockerfile
|
|
54
|
+
# 2. Scan your Dockerfile (local — no login needed)
|
|
57
55
|
shieldops analyze Dockerfile
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
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
|
+
```
|
|
61
69
|
|
|
62
70
|
---
|
|
63
71
|
|
|
@@ -87,11 +95,14 @@ pip install shieldops-cli
|
|
|
87
95
|
|
|
88
96
|
### `analyze` — Dockerfile Security Scan
|
|
89
97
|
|
|
98
|
+
Runs locally by default (no API key). Use `--api` for cloud AI analysis.
|
|
99
|
+
|
|
90
100
|
```bash
|
|
91
|
-
shieldops analyze Dockerfile
|
|
92
|
-
shieldops analyze Dockerfile --
|
|
93
|
-
shieldops analyze Dockerfile --
|
|
94
|
-
shieldops analyze Dockerfile --
|
|
101
|
+
shieldops analyze Dockerfile # local (free, unlimited)
|
|
102
|
+
shieldops analyze Dockerfile --api # cloud AI (requires login)
|
|
103
|
+
shieldops analyze Dockerfile --format json --output report.json
|
|
104
|
+
shieldops analyze Dockerfile --fail-on high # CI/CD gate
|
|
105
|
+
shieldops analyze Dockerfile --open-report # open browser report
|
|
95
106
|
```
|
|
96
107
|
|
|
97
108
|
### `autofix` — AI-Powered Dockerfile Fix
|
|
@@ -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]")
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shieldops-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
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
6
|
License-Expression: MIT
|
|
@@ -40,11 +40,11 @@ Dynamic: license-file
|
|
|
40
40
|
[](https://shieldops-ai.dev)
|
|
41
41
|
|
|
42
42
|
<p align="center">
|
|
43
|
-
<img src="docs/screenshots/tui-session.
|
|
43
|
+
<img src="https://raw.githubusercontent.com/mohammedabdallahcv-creator/shieldops-cli/main/docs/screenshots/tui-session.png" alt="ShieldOps TUI interactive session" width="800">
|
|
44
44
|
</p>
|
|
45
45
|
|
|
46
46
|
<p align="center">
|
|
47
|
-
<img src="docs/screenshots/cli-output.
|
|
47
|
+
<img src="https://raw.githubusercontent.com/mohammedabdallahcv-creator/shieldops-cli/main/docs/screenshots/cli-output.png" alt="ShieldOps CLI scan results" width="800">
|
|
48
48
|
</p>
|
|
49
49
|
|
|
50
50
|
---
|
|
@@ -64,7 +64,8 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
64
64
|
| Docker image scan | Yes | No | Yes (built-in) |
|
|
65
65
|
| Interactive TUI | Yes | No | No |
|
|
66
66
|
| CI/CD ready (`--fail-on`) | Yes | Yes | Yes |
|
|
67
|
-
| Free tier |
|
|
67
|
+
| Free tier (local) | Unlimited scans, no signup | Yes | Yes |
|
|
68
|
+
| Cloud AI analysis | With API key (5 free/day) | — | — |
|
|
68
69
|
|
|
69
70
|
### What makes it different
|
|
70
71
|
|
|
@@ -81,14 +82,21 @@ Most Dockerfile/K8s scanners tell you **what** is wrong. ShieldOps CLI also tell
|
|
|
81
82
|
# 1. Install
|
|
82
83
|
pip install shieldops-cli
|
|
83
84
|
|
|
84
|
-
# 2.
|
|
85
|
-
shieldops login
|
|
86
|
-
|
|
87
|
-
# 3. Scan your Dockerfile
|
|
85
|
+
# 2. Scan your Dockerfile (local — no login needed)
|
|
88
86
|
shieldops analyze Dockerfile
|
|
89
87
|
```
|
|
90
88
|
|
|
91
|
-
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
|
+
```
|
|
92
100
|
|
|
93
101
|
---
|
|
94
102
|
|
|
@@ -118,11 +126,14 @@ pip install shieldops-cli
|
|
|
118
126
|
|
|
119
127
|
### `analyze` — Dockerfile Security Scan
|
|
120
128
|
|
|
129
|
+
Runs locally by default (no API key). Use `--api` for cloud AI analysis.
|
|
130
|
+
|
|
121
131
|
```bash
|
|
122
|
-
shieldops analyze Dockerfile
|
|
123
|
-
shieldops analyze Dockerfile --
|
|
124
|
-
shieldops analyze Dockerfile --
|
|
125
|
-
shieldops analyze Dockerfile --
|
|
132
|
+
shieldops analyze Dockerfile # local (free, unlimited)
|
|
133
|
+
shieldops analyze Dockerfile --api # cloud AI (requires login)
|
|
134
|
+
shieldops analyze Dockerfile --format json --output report.json
|
|
135
|
+
shieldops analyze Dockerfile --fail-on high # CI/CD gate
|
|
136
|
+
shieldops analyze Dockerfile --open-report # open browser report
|
|
126
137
|
```
|
|
127
138
|
|
|
128
139
|
### `autofix` — AI-Powered Dockerfile Fix
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|