CAS-174: cascadeguard scan — Easiest Use Case
Overview
Add a scan CLI command that analyses a repository directory, discovers all container-related artifacts, presents them interactively for selection, and produces analysis results. This is the zero-config onboarding path for new users.
Additionally, create a one-shot wrapper script (install.sh) that downloads the tool and runs scan in a single command.
Updated per CAS-222: incorporates the state-driven model, repo-per-image catalogue structure, and check-upstream integration decisions.
CLI Interface
cascadeguard scan [--dir PATH] [--non-interactive] [--format json|text|yaml] [--output FILE]
--dirdefaults to.(current working directory)--non-interactivescans all discovered artifacts without prompting--formatcontrols output format (default:text)--outputwrites results to file instead of stdout
Architecture
Discovery Protocol
Each discovery module implements a Discoverer protocol:
class Discoverer(Protocol):
name: str # e.g. "Dockerfiles"
def discover(self, root: Path) -> list[DiscoveredArtifact]: ...DiscoveredArtifact — dataclass: kind (dockerfile|actions|compose|k8s), path (relative), details (parsed metadata dict).
Discovery Modules
| Module | Glob Patterns | Reuses |
|---|---|---|
| Dockerfile | **/Dockerfile, **/Dockerfile.*, **/*.dockerfile, **/Containerfile* | CascadeGuardTool.parse_dockerfile_base_images() |
| GitHub Actions | .github/workflows/*.yml, .github/workflows/*.yaml | ActionsPinner._USES_RE regex |
| Compose/Stack | **/docker-compose*.yml, **/compose*.yml variants | — |
| K8s Manifests | **/*.yaml filtered by apiVersion + kind heuristic | — |
Performance: os.walk with early pruning of .git, node_modules, vendor, __pycache__, .venv.
Stretch: GitLab CI (.gitlab-ci.yml).
Interactive Selection UI
Python stdlib curses checkbox selector. All items default to selected. Arrow keys navigate, space toggles, enter confirms.
Fallback: When curses is unavailable (piped stdin, Windows, no TTY), fall back to --non-interactive mode that scans everything.
Analysis Output
Structured report per artifact:
scan:
directory: /path/to/repo
timestamp: "2026-04-04T18:45:00Z"
artifacts:
- kind: dockerfile
path: Dockerfile
base_images:
- image: python:3.11-slim
pinned: false
recommendation: "Pin to digest"
- kind: github_actions
path: .github/workflows/ci.yaml
actions:
- uses: actions/checkout@v4
pinned: false
recommendation: "Pin to SHA (use cascadeguard actions pin)"
summary:
total_artifacts: 12
selected: 10
images_found: 15
unpinned_images: 8
unpinned_actions: 2End-to-End Flow (per CAS-222)
This section documents how scan fits into the broader CascadeGuard lifecycle. The scan command is the discovery entry point; downstream stages use the state-driven model agreed in CAS-222.
User runs wrapper script or `cascadeguard scan`
│
▼
Discovery: walk repo, find Dockerfiles / Actions / Compose / K8s
│
▼
Interactive selection (or --non-interactive)
│
▼
Analysis: parse each artifact, extract image refs + action refs
│
▼
Report: summary + per-artifact recommendations
│
▼
(Optional) User acts on recommendations:
│
├── `cascadeguard actions pin` — pin unpinned GH Actions to SHA
├── `cascadeguard enrol` — enrol discovered images into catalogue
└── `cascadeguard check-upstream` — detect new upstream tags/digests
│
▼
CLI writes state to catalogue repo (state-driven, no notifications)
│
▼
CI reacts to committed state changes:
- Rebuild if digest drifted
- Open enrolment PR for new stable tags
- Update dashboard/catalogue
Key Architectural Decisions (from CAS-222)
| Decision | Choice | Rationale |
|---|---|---|
| Catalogue structure | Repo-per-image, central catalogue | CI isolation, clean release tagging |
| State model | CLI writes state, CI reacts to commits | No notification/issue-creation from CLI |
| Upstream detection | check-upstream CLI command | Fits noun-verb CLI pattern |
| Kargo | Not used | No k8s in CascadeGuard environment |
File Structure
app/
├── app.py # Add scan subcommand + cmd_scan handler
├── scan/
│ ├── __init__.py # Public API: run_scan()
│ ├── discoverers.py # All Discoverer implementations
│ ├── models.py # DiscoveredArtifact, ScanResult dataclasses
│ ├── ui.py # Interactive selection (curses + fallback)
│ └── report.py # Analysis and output formatting
├── tests/
│ ├── test_scan_discovery.py # Unit tests for each discoverer
│ ├── test_scan_report.py # Report generation tests
│ └── test_scan_ui.py # UI tests (mocked curses)
install.sh # One-shot wrapper script
Implementation Phases
Phase 1: Core Discovery
- Define
DiscoveredArtifactmodel andDiscovererprotocol inmodels.py - Implement Dockerfile and GitHub Actions discoverers (reuse existing parsing)
- Wire
scansubcommand intobuild_parser()andcmd_scan()handler - Unit tests for both discoverers
Phase 2: Extended Discovery
- Compose/Stack discoverer
- Kubernetes manifest discoverer
- Integration tests with fixture repos
Phase 3: Interactive UI
- Curses-based checkbox selector in
ui.py - Fallback plain-text mode for non-TTY environments
--non-interactiveflag
Phase 4: Analysis & Reporting
- Structured output (YAML/JSON/text)
- Recommendations engine (pin suggestions, enrol suggestions)
- Summary statistics
- Bridge to existing commands: output includes actionable
cascadeguardcommands the user can run next
Phase 5: Wrapper Script
install.sh— detect OS/arch, download release, runcascadeguard scan- Distribution: GitHub releases
- Smoke tests
Technical Decisions
- No new Python dependencies for Phases 1–4 (
cursesis stdlib,pyyamlalready present) - Reuse existing code: Dockerfile parsing from
CascadeGuardTool, action ref regex fromActionsPinner - K8s detection: Filter
**/*.yamlbyapiVersion+kindto avoid false positives - Large repos:
os.walkwith early directory pruning - curses on Windows: Fallback to
--non-interactive; optionally supportwindows-curses - Scope: Dockerfile + Actions + Compose + K8s. GitLab CI is stretch.
Dependencies
- Depends on: existing
CascadeGuardToolclass,ActionsPinnerregex - Feeds into:
check-upstream(CAS-222),actions pin,enrolcommands - No new external Python packages required