vehlo-code-scanner 0.1.1rc1__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.
- vehlo_code_scanner-0.1.1rc1/.gitignore +53 -0
- vehlo_code_scanner-0.1.1rc1/PKG-INFO +198 -0
- vehlo_code_scanner-0.1.1rc1/README.md +170 -0
- vehlo_code_scanner-0.1.1rc1/pyproject.toml +81 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/__init__.py +1 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/__init__.py +1 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/app.py +92 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/auth.py +95 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/deps.py +161 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/__init__.py +1 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/analytics.py +79 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/auth.py +169 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/findings.py +141 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/groups.py +122 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/health.py +11 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/ingest.py +49 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/overview.py +95 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/repos.py +106 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/scans.py +79 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/tokens.py +178 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/schemas.py +172 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/services/__init__.py +1 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/services/auto_resolve.py +55 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/services/ingest.py +205 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/api/session.py +32 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/cli/__init__.py +1 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/cli/admin.py +177 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/cli/app.py +19 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/cli/output.py +68 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/cli/scan_command.py +195 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/client/__init__.py +1 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/client/api.py +108 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/config.py +150 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/db.py +47 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/enums.py +55 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/fingerprint.py +17 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/__init__.py +22 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/api_token.py +39 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/base.py +32 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/finding.py +81 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/finding_history.py +42 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/group.py +35 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/organization.py +23 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/repo.py +56 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/scan.py +62 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/models/user.py +53 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/scanner/__init__.py +1 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/scanner/models.py +67 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/scanner/parser.py +25 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/scanner/runner.py +146 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/services/__init__.py +1 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/services/provisioning.py +144 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/services/tokens.py +74 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/storage/__init__.py +55 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/storage/memory.py +19 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/storage/s3.py +49 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/worker/__init__.py +1 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/worker/celery_app.py +17 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/worker/db.py +33 -0
- vehlo_code_scanner-0.1.1rc1/src/vcs/worker/tasks.py +48 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.eggs/
|
|
8
|
+
|
|
9
|
+
# Virtual environment
|
|
10
|
+
.venv/
|
|
11
|
+
|
|
12
|
+
# IDE
|
|
13
|
+
.idea/
|
|
14
|
+
.vscode/
|
|
15
|
+
*.swp
|
|
16
|
+
*.swo
|
|
17
|
+
|
|
18
|
+
# Environment
|
|
19
|
+
.env
|
|
20
|
+
.env.*
|
|
21
|
+
|
|
22
|
+
# Testing
|
|
23
|
+
.pytest_cache/
|
|
24
|
+
.coverage
|
|
25
|
+
htmlcov/
|
|
26
|
+
|
|
27
|
+
# Docker
|
|
28
|
+
docker-compose.override.yml
|
|
29
|
+
|
|
30
|
+
# Alembic
|
|
31
|
+
alembic/versions/__pycache__/
|
|
32
|
+
|
|
33
|
+
# OS
|
|
34
|
+
.DS_Store
|
|
35
|
+
Thumbs.db
|
|
36
|
+
|
|
37
|
+
# Office documents (working files + Word lock files)
|
|
38
|
+
*.docx
|
|
39
|
+
~$*
|
|
40
|
+
|
|
41
|
+
# MinIO data
|
|
42
|
+
minio_data/
|
|
43
|
+
|
|
44
|
+
# Dashboard
|
|
45
|
+
dashboard/node_modules/
|
|
46
|
+
dashboard/dist/
|
|
47
|
+
|
|
48
|
+
# Local tooling caches
|
|
49
|
+
.ash/
|
|
50
|
+
.claude-fuel/
|
|
51
|
+
|
|
52
|
+
# Local Claude context (deployment/operational notes — not public)
|
|
53
|
+
CLAUDE.local.md
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vehlo-code-scanner
|
|
3
|
+
Version: 0.1.1rc1
|
|
4
|
+
Summary: Multi-tenant security scanning platform wrapping Amazon Security Helper
|
|
5
|
+
Project-URL: Homepage, https://github.com/Vehlo-CyberSec/vehlo-code-scanner
|
|
6
|
+
Project-URL: Repository, https://github.com/Vehlo-CyberSec/vehlo-code-scanner
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: alembic<2.0,>=1.13
|
|
9
|
+
Requires-Dist: authlib<2.0,>=1.3
|
|
10
|
+
Requires-Dist: boto3>=1.34
|
|
11
|
+
Requires-Dist: celery<6.0,>=5.3
|
|
12
|
+
Requires-Dist: fastapi<1.0,>=0.115
|
|
13
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
14
|
+
Requires-Dist: itsdangerous<3.0,>=2.1
|
|
15
|
+
Requires-Dist: psycopg[binary]<4.0,>=3.1
|
|
16
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
17
|
+
Requires-Dist: redis<6.0,>=5.0
|
|
18
|
+
Requires-Dist: rich<14.0,>=13.5
|
|
19
|
+
Requires-Dist: sqlalchemy<3.0,>=2.0
|
|
20
|
+
Requires-Dist: typer<1.0,>=0.16
|
|
21
|
+
Requires-Dist: uvicorn[standard]<1.0,>=0.30
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-cov<6.0,>=5.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == 'dev'
|
|
25
|
+
Provides-Extra: scan
|
|
26
|
+
Requires-Dist: vehlo-ash<4,>=3.2.5; extra == 'scan'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Vehlo Code Scanner
|
|
30
|
+
|
|
31
|
+
Multi-tenant security scanning platform wrapping [Amazon Security Helper (ASH)](https://github.com/awslabs/automated-security-helper). Runs ASH across Vehlo's 500+ repos, centralizes findings in PostgreSQL, and surfaces them through a React dashboard with triage workflows and analytics.
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
CI/CD pipeline → ASH scan → vcs CLI (--push) → POST /api/v1/scans → FastAPI + PostgreSQL → React Dashboard
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Three components:
|
|
40
|
+
|
|
41
|
+
- **CLI / scanner** — Python package (`vcs`) that wraps ASH. Supports container, local, and pre-commit modes. Outputs rich terminal tables, optionally pushes results to the central API, and can fail the build on a severity threshold.
|
|
42
|
+
- **API service** — FastAPI monolith handling ingest, findings lifecycle, and analytics. Backed by PostgreSQL. Auto-resolves findings when they disappear from subsequent scans.
|
|
43
|
+
- **Dashboard** — React + Vite SPA. Overview, findings browser, per-repo drill-down, scan history, and analytics.
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Start all services
|
|
49
|
+
docker compose up
|
|
50
|
+
|
|
51
|
+
# API → http://localhost:8002
|
|
52
|
+
# Dashboard → http://localhost:5175
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Installation
|
|
56
|
+
|
|
57
|
+
Distribution is **dual-mode**: a public path (PyPI + ECR Public, no AWS
|
|
58
|
+
account needed) and an AWS-gated path (CodeArtifact + private ECR) for
|
|
59
|
+
internal users. The tool itself is open to install — the **API token** gates
|
|
60
|
+
pushing results and **SSO** gates viewing them. The `[scan]` extra adds the
|
|
61
|
+
ASH engine (`vehlo-ash`, a rename-only repackaging of AWS's Apache-2.0
|
|
62
|
+
automated-security-helper — see `packaging/vehlo-ash/`); without it you still
|
|
63
|
+
get the CLI, `--push`, and the client.
|
|
64
|
+
|
|
65
|
+
**Public (no AWS):**
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# pip from PyPI
|
|
69
|
+
pip install 'vehlo-code-scanner[scan]'
|
|
70
|
+
|
|
71
|
+
# Docker from ECR Public (ASH bundled, nothing else to install)
|
|
72
|
+
docker run --rm -v "$PWD:/src" \
|
|
73
|
+
public.ecr.aws/<alias>/vehlo-code-scanner:latest scan /src
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**AWS-gated (internal):** authenticate once with `aws sso login`, then:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# pip via CodeArtifact (configures the index, then installs)
|
|
80
|
+
aws codeartifact login --tool pip \
|
|
81
|
+
--domain "$VCS_CA_DOMAIN" --repository "$VCS_CA_REPO"
|
|
82
|
+
pip install 'vehlo-code-scanner[scan]'
|
|
83
|
+
|
|
84
|
+
# Docker via ECR
|
|
85
|
+
aws ecr get-login-password --region "$AWS_REGION" \
|
|
86
|
+
| docker login --username AWS --password-stdin "$ECR_REGISTRY"
|
|
87
|
+
docker run --rm -v "$PWD:/src" \
|
|
88
|
+
"$ECR_REGISTRY/vehlo-code-scanner:latest" scan /src
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**GitHub Actions** (the calling job needs `permissions: id-token: write`):
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
- uses: Vehlo-CyberSec/vehlo-code-scanner@v1
|
|
95
|
+
with:
|
|
96
|
+
api-url: ${{ vars.VCS_API_URL }}
|
|
97
|
+
api-token: ${{ secrets.VCS_API_TOKEN }}
|
|
98
|
+
image: ${{ vars.VCS_ECR_IMAGE }} # full ECR URI
|
|
99
|
+
aws-role: ${{ secrets.VCS_AWS_ROLE }} # OIDC role with ECR pull
|
|
100
|
+
aws-region: ${{ vars.AWS_REGION }}
|
|
101
|
+
fail-on: high
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> ASH is git-only upstream (and its PyPI name is squatted), so `vcs scan`
|
|
105
|
+
> without the engine prints install guidance. The container images bundle ASH;
|
|
106
|
+
> pip users get it via the `[scan]` extra (`vehlo-ash` from PyPI or
|
|
107
|
+
> CodeArtifact, depending on the index you install from).
|
|
108
|
+
|
|
109
|
+
For local development from a checkout:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uv tool install '.[scan]' # or: uv sync --group local-scan (ASH from git)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
After installation, `vcs` (and `vcs-admin`) are available on your PATH.
|
|
116
|
+
|
|
117
|
+
### Local development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Install Python deps
|
|
121
|
+
uv sync
|
|
122
|
+
|
|
123
|
+
# Apply migrations
|
|
124
|
+
VCS_DATABASE_URL=postgresql+psycopg://vcs:<password>@localhost:5433/vcs alembic upgrade head
|
|
125
|
+
# (dev password: see docker-compose.yml)
|
|
126
|
+
|
|
127
|
+
# Run API
|
|
128
|
+
VCS_DATABASE_URL=... uvicorn vcs.api.app:create_app --factory --reload
|
|
129
|
+
|
|
130
|
+
# Run dashboard (in ./dashboard)
|
|
131
|
+
npm run dev
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Running a scan
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Scan current directory and print results
|
|
138
|
+
vcs scan .
|
|
139
|
+
|
|
140
|
+
# Scan and push results to central API
|
|
141
|
+
VCS_API_URL=http://localhost:8002 VCS_API_TOKEN=<token> vcs scan . --push
|
|
142
|
+
|
|
143
|
+
# Fail CI if critical or high findings exist
|
|
144
|
+
vcs scan . --push --fail-on high
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Project Structure
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
src/vcs/
|
|
151
|
+
├── api/
|
|
152
|
+
│ ├── routes/ # health, ingest, findings, repos, scans, overview, analytics
|
|
153
|
+
│ ├── services/ # ingest logic, auto-resolve
|
|
154
|
+
│ ├── app.py # FastAPI factory
|
|
155
|
+
│ ├── deps.py # DB session injection
|
|
156
|
+
│ └── schemas.py # Pydantic request/response models
|
|
157
|
+
├── models/ # SQLAlchemy ORM: org, group, user, repo, scan, finding, api_token
|
|
158
|
+
├── scanner/ # ASH wrapper: runner, parser, result models
|
|
159
|
+
├── cli/ # Typer CLI: scan command, rich output
|
|
160
|
+
├── client/ # HTTP client for pushing results to API
|
|
161
|
+
├── db.py # Database connection factory
|
|
162
|
+
├── enums.py # Severity, FindingStatus, etc.
|
|
163
|
+
└── config.py # Settings from environment
|
|
164
|
+
|
|
165
|
+
dashboard/src/
|
|
166
|
+
├── pages/ # Overview, Findings, FindingDetail, Repos, RepoDetail, Scans, Analytics
|
|
167
|
+
├── components/ # Layout, SeverityBadge, StatusBadge, Pagination, Panel, etc.
|
|
168
|
+
├── api/ # TanStack Query hooks
|
|
169
|
+
└── types.ts # TypeScript types
|
|
170
|
+
|
|
171
|
+
alembic/versions/ # Database migrations
|
|
172
|
+
tests/ # Unit + integration tests
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Environment Variables
|
|
176
|
+
|
|
177
|
+
| Variable | Description | Default |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| `VCS_DATABASE_URL` | PostgreSQL connection string | required |
|
|
180
|
+
| `VCS_API_URL` | Central API base URL (CLI push) | required for `--push` |
|
|
181
|
+
| `VCS_API_TOKEN` | Bearer token for API auth (CLI push) | required for `--push` |
|
|
182
|
+
| `VCS_REDIS_URL` | Celery broker/result backend | `redis://localhost:6380/0` |
|
|
183
|
+
| `VCS_S3_ENDPOINT` | S3/MinIO endpoint for raw-result archiving | `http://localhost:9002` |
|
|
184
|
+
| `VCS_SESSION_SECRET` | Secret for signing dashboard session cookies | dev default (**required** once OIDC is configured — startup fails without it) |
|
|
185
|
+
| `VCS_CORS_ORIGINS` | Comma-separated allowed CORS origins; empty value = deny cross-origin (prod) | unset → any localhost port (dev) |
|
|
186
|
+
| `VCS_OIDC_ISSUER` | OIDC issuer URL (enables dashboard SSO) | unset → SSO disabled |
|
|
187
|
+
| `VCS_OIDC_CLIENT_ID` / `VCS_OIDC_CLIENT_SECRET` | OIDC client credentials | unset |
|
|
188
|
+
| `VCS_OIDC_REDIRECT_URI` | OIDC callback URL | `.../api/v1/auth/callback` |
|
|
189
|
+
| `VCS_OIDC_GROUPS_CLAIM` | Claim holding the user's group names | `groups` |
|
|
190
|
+
| `VCS_DASHBOARD_URL` | Post-login redirect target | `http://localhost:5175` |
|
|
191
|
+
| `VCS_DEV_LOGIN` | **Dev only** — enables `/api/v1/auth/dev-login` (one-click local session, no IdP). Never set in prod. | unset → disabled |
|
|
192
|
+
|
|
193
|
+
## Running Tests
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pytest
|
|
197
|
+
pytest --cov=vcs --cov-report=term-missing
|
|
198
|
+
```
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Vehlo Code Scanner
|
|
2
|
+
|
|
3
|
+
Multi-tenant security scanning platform wrapping [Amazon Security Helper (ASH)](https://github.com/awslabs/automated-security-helper). Runs ASH across Vehlo's 500+ repos, centralizes findings in PostgreSQL, and surfaces them through a React dashboard with triage workflows and analytics.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
CI/CD pipeline → ASH scan → vcs CLI (--push) → POST /api/v1/scans → FastAPI + PostgreSQL → React Dashboard
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Three components:
|
|
12
|
+
|
|
13
|
+
- **CLI / scanner** — Python package (`vcs`) that wraps ASH. Supports container, local, and pre-commit modes. Outputs rich terminal tables, optionally pushes results to the central API, and can fail the build on a severity threshold.
|
|
14
|
+
- **API service** — FastAPI monolith handling ingest, findings lifecycle, and analytics. Backed by PostgreSQL. Auto-resolves findings when they disappear from subsequent scans.
|
|
15
|
+
- **Dashboard** — React + Vite SPA. Overview, findings browser, per-repo drill-down, scan history, and analytics.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Start all services
|
|
21
|
+
docker compose up
|
|
22
|
+
|
|
23
|
+
# API → http://localhost:8002
|
|
24
|
+
# Dashboard → http://localhost:5175
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Installation
|
|
28
|
+
|
|
29
|
+
Distribution is **dual-mode**: a public path (PyPI + ECR Public, no AWS
|
|
30
|
+
account needed) and an AWS-gated path (CodeArtifact + private ECR) for
|
|
31
|
+
internal users. The tool itself is open to install — the **API token** gates
|
|
32
|
+
pushing results and **SSO** gates viewing them. The `[scan]` extra adds the
|
|
33
|
+
ASH engine (`vehlo-ash`, a rename-only repackaging of AWS's Apache-2.0
|
|
34
|
+
automated-security-helper — see `packaging/vehlo-ash/`); without it you still
|
|
35
|
+
get the CLI, `--push`, and the client.
|
|
36
|
+
|
|
37
|
+
**Public (no AWS):**
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# pip from PyPI
|
|
41
|
+
pip install 'vehlo-code-scanner[scan]'
|
|
42
|
+
|
|
43
|
+
# Docker from ECR Public (ASH bundled, nothing else to install)
|
|
44
|
+
docker run --rm -v "$PWD:/src" \
|
|
45
|
+
public.ecr.aws/<alias>/vehlo-code-scanner:latest scan /src
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**AWS-gated (internal):** authenticate once with `aws sso login`, then:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# pip via CodeArtifact (configures the index, then installs)
|
|
52
|
+
aws codeartifact login --tool pip \
|
|
53
|
+
--domain "$VCS_CA_DOMAIN" --repository "$VCS_CA_REPO"
|
|
54
|
+
pip install 'vehlo-code-scanner[scan]'
|
|
55
|
+
|
|
56
|
+
# Docker via ECR
|
|
57
|
+
aws ecr get-login-password --region "$AWS_REGION" \
|
|
58
|
+
| docker login --username AWS --password-stdin "$ECR_REGISTRY"
|
|
59
|
+
docker run --rm -v "$PWD:/src" \
|
|
60
|
+
"$ECR_REGISTRY/vehlo-code-scanner:latest" scan /src
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**GitHub Actions** (the calling job needs `permissions: id-token: write`):
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
- uses: Vehlo-CyberSec/vehlo-code-scanner@v1
|
|
67
|
+
with:
|
|
68
|
+
api-url: ${{ vars.VCS_API_URL }}
|
|
69
|
+
api-token: ${{ secrets.VCS_API_TOKEN }}
|
|
70
|
+
image: ${{ vars.VCS_ECR_IMAGE }} # full ECR URI
|
|
71
|
+
aws-role: ${{ secrets.VCS_AWS_ROLE }} # OIDC role with ECR pull
|
|
72
|
+
aws-region: ${{ vars.AWS_REGION }}
|
|
73
|
+
fail-on: high
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> ASH is git-only upstream (and its PyPI name is squatted), so `vcs scan`
|
|
77
|
+
> without the engine prints install guidance. The container images bundle ASH;
|
|
78
|
+
> pip users get it via the `[scan]` extra (`vehlo-ash` from PyPI or
|
|
79
|
+
> CodeArtifact, depending on the index you install from).
|
|
80
|
+
|
|
81
|
+
For local development from a checkout:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
uv tool install '.[scan]' # or: uv sync --group local-scan (ASH from git)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
After installation, `vcs` (and `vcs-admin`) are available on your PATH.
|
|
88
|
+
|
|
89
|
+
### Local development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Install Python deps
|
|
93
|
+
uv sync
|
|
94
|
+
|
|
95
|
+
# Apply migrations
|
|
96
|
+
VCS_DATABASE_URL=postgresql+psycopg://vcs:<password>@localhost:5433/vcs alembic upgrade head
|
|
97
|
+
# (dev password: see docker-compose.yml)
|
|
98
|
+
|
|
99
|
+
# Run API
|
|
100
|
+
VCS_DATABASE_URL=... uvicorn vcs.api.app:create_app --factory --reload
|
|
101
|
+
|
|
102
|
+
# Run dashboard (in ./dashboard)
|
|
103
|
+
npm run dev
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Running a scan
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Scan current directory and print results
|
|
110
|
+
vcs scan .
|
|
111
|
+
|
|
112
|
+
# Scan and push results to central API
|
|
113
|
+
VCS_API_URL=http://localhost:8002 VCS_API_TOKEN=<token> vcs scan . --push
|
|
114
|
+
|
|
115
|
+
# Fail CI if critical or high findings exist
|
|
116
|
+
vcs scan . --push --fail-on high
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Project Structure
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
src/vcs/
|
|
123
|
+
├── api/
|
|
124
|
+
│ ├── routes/ # health, ingest, findings, repos, scans, overview, analytics
|
|
125
|
+
│ ├── services/ # ingest logic, auto-resolve
|
|
126
|
+
│ ├── app.py # FastAPI factory
|
|
127
|
+
│ ├── deps.py # DB session injection
|
|
128
|
+
│ └── schemas.py # Pydantic request/response models
|
|
129
|
+
├── models/ # SQLAlchemy ORM: org, group, user, repo, scan, finding, api_token
|
|
130
|
+
├── scanner/ # ASH wrapper: runner, parser, result models
|
|
131
|
+
├── cli/ # Typer CLI: scan command, rich output
|
|
132
|
+
├── client/ # HTTP client for pushing results to API
|
|
133
|
+
├── db.py # Database connection factory
|
|
134
|
+
├── enums.py # Severity, FindingStatus, etc.
|
|
135
|
+
└── config.py # Settings from environment
|
|
136
|
+
|
|
137
|
+
dashboard/src/
|
|
138
|
+
├── pages/ # Overview, Findings, FindingDetail, Repos, RepoDetail, Scans, Analytics
|
|
139
|
+
├── components/ # Layout, SeverityBadge, StatusBadge, Pagination, Panel, etc.
|
|
140
|
+
├── api/ # TanStack Query hooks
|
|
141
|
+
└── types.ts # TypeScript types
|
|
142
|
+
|
|
143
|
+
alembic/versions/ # Database migrations
|
|
144
|
+
tests/ # Unit + integration tests
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Environment Variables
|
|
148
|
+
|
|
149
|
+
| Variable | Description | Default |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `VCS_DATABASE_URL` | PostgreSQL connection string | required |
|
|
152
|
+
| `VCS_API_URL` | Central API base URL (CLI push) | required for `--push` |
|
|
153
|
+
| `VCS_API_TOKEN` | Bearer token for API auth (CLI push) | required for `--push` |
|
|
154
|
+
| `VCS_REDIS_URL` | Celery broker/result backend | `redis://localhost:6380/0` |
|
|
155
|
+
| `VCS_S3_ENDPOINT` | S3/MinIO endpoint for raw-result archiving | `http://localhost:9002` |
|
|
156
|
+
| `VCS_SESSION_SECRET` | Secret for signing dashboard session cookies | dev default (**required** once OIDC is configured — startup fails without it) |
|
|
157
|
+
| `VCS_CORS_ORIGINS` | Comma-separated allowed CORS origins; empty value = deny cross-origin (prod) | unset → any localhost port (dev) |
|
|
158
|
+
| `VCS_OIDC_ISSUER` | OIDC issuer URL (enables dashboard SSO) | unset → SSO disabled |
|
|
159
|
+
| `VCS_OIDC_CLIENT_ID` / `VCS_OIDC_CLIENT_SECRET` | OIDC client credentials | unset |
|
|
160
|
+
| `VCS_OIDC_REDIRECT_URI` | OIDC callback URL | `.../api/v1/auth/callback` |
|
|
161
|
+
| `VCS_OIDC_GROUPS_CLAIM` | Claim holding the user's group names | `groups` |
|
|
162
|
+
| `VCS_DASHBOARD_URL` | Post-login redirect target | `http://localhost:5175` |
|
|
163
|
+
| `VCS_DEV_LOGIN` | **Dev only** — enables `/api/v1/auth/dev-login` (one-click local session, no IdP). Never set in prod. | unset → disabled |
|
|
164
|
+
|
|
165
|
+
## Running Tests
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pytest
|
|
169
|
+
pytest --cov=vcs --cov-report=term-missing
|
|
170
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vehlo-code-scanner"
|
|
3
|
+
version = "0.1.1rc1"
|
|
4
|
+
description = "Multi-tenant security scanning platform wrapping Amazon Security Helper"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"sqlalchemy>=2.0,<3.0",
|
|
9
|
+
"psycopg[binary]>=3.1,<4.0",
|
|
10
|
+
"alembic>=1.13,<2.0",
|
|
11
|
+
"typer>=0.16,<1.0",
|
|
12
|
+
"rich>=13.5,<14.0",
|
|
13
|
+
"httpx>=0.27,<1.0",
|
|
14
|
+
"fastapi>=0.115,<1.0",
|
|
15
|
+
"uvicorn[standard]>=0.30,<1.0",
|
|
16
|
+
"pydantic>=2.0,<3.0",
|
|
17
|
+
"celery>=5.3,<6.0",
|
|
18
|
+
"redis>=5.0,<6.0",
|
|
19
|
+
"boto3>=1.34",
|
|
20
|
+
"authlib>=1.3,<2.0",
|
|
21
|
+
"itsdangerous>=2.1,<3.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
scan = [
|
|
26
|
+
"vehlo-ash>=3.2.5,<4",
|
|
27
|
+
]
|
|
28
|
+
# NOTE: the published wheel also gets a `scan` extra (`vehlo-ash>=3.2.5,<4`),
|
|
29
|
+
# injected by the release workflow at build time — see
|
|
30
|
+
# .github/workflows/release.yml and packaging/vehlo-ash/README.md. It is not
|
|
31
|
+
# declared here because vehlo-ash is published by that same release run, so
|
|
32
|
+
# declaring it would make `uv lock` unsatisfiable in dev. Dev installs ASH via
|
|
33
|
+
# the `local-scan` dependency-group below instead.
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8.0,<9.0",
|
|
36
|
+
"pytest-cov>=5.0,<6.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/Vehlo-CyberSec/vehlo-code-scanner"
|
|
41
|
+
Repository = "https://github.com/Vehlo-CyberSec/vehlo-code-scanner"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/vcs"]
|
|
49
|
+
|
|
50
|
+
# Hatchling's sdist default is "everything not gitignored", which would
|
|
51
|
+
# publish the whole repo (internal docs, CI config, dashboard sources, any
|
|
52
|
+
# stray local files) to PyPI. Ship only what the package needs.
|
|
53
|
+
[tool.hatch.build.targets.sdist]
|
|
54
|
+
only-include = ["src/vcs", "README.md"]
|
|
55
|
+
|
|
56
|
+
[project.scripts]
|
|
57
|
+
vcs = "vcs.cli.app:app"
|
|
58
|
+
vcs-admin = "vcs.cli.admin:app"
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
62
|
+
pythonpath = ["src"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
line-length = 80
|
|
66
|
+
target-version = "py312"
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = ["E", "F", "I", "UP"]
|
|
70
|
+
|
|
71
|
+
# Dev-only ASH install, straight from AWS's git at a pinned commit. This group
|
|
72
|
+
# is NOT part of published wheel metadata and is not installed by a plain
|
|
73
|
+
# `pip install`, so the squatted PyPI name never reaches end users. The Docker
|
|
74
|
+
# image bakes ASH via this group; published pip users get vehlo-ash instead.
|
|
75
|
+
[dependency-groups]
|
|
76
|
+
local-scan = [
|
|
77
|
+
"automated-security-helper>=3.0",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.uv.sources]
|
|
81
|
+
automated-security-helper = { git = "https://github.com/awslabs/automated-security-helper.git", rev = "968c5f8ce337499577f97e62afcf66c0a2bb9a84" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Vehlo Code Scanner — multi-tenant security scanning platform."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API module — FastAPI application and routes."""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""FastAPI application factory."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
from fastapi.responses import FileResponse
|
|
9
|
+
from fastapi.staticfiles import StaticFiles
|
|
10
|
+
from starlette.middleware.sessions import SessionMiddleware
|
|
11
|
+
|
|
12
|
+
from vcs.api.routes import (
|
|
13
|
+
analytics,
|
|
14
|
+
auth,
|
|
15
|
+
findings,
|
|
16
|
+
groups,
|
|
17
|
+
health,
|
|
18
|
+
ingest,
|
|
19
|
+
overview,
|
|
20
|
+
repos,
|
|
21
|
+
scans,
|
|
22
|
+
tokens,
|
|
23
|
+
)
|
|
24
|
+
from vcs.config import get_cors_origins, get_session_secret
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_app() -> FastAPI:
|
|
28
|
+
"""Create and configure the FastAPI application."""
|
|
29
|
+
app = FastAPI(
|
|
30
|
+
title="Vehlo Code Scanner API",
|
|
31
|
+
description="Central API for security scan result ingestion and findings management.",
|
|
32
|
+
version="0.1.0",
|
|
33
|
+
)
|
|
34
|
+
# SessionMiddleware holds transient OIDC state (CSRF/nonce) during the
|
|
35
|
+
# login handshake. The authenticated session is a separate signed cookie.
|
|
36
|
+
app.add_middleware(SessionMiddleware, secret_key=get_session_secret())
|
|
37
|
+
# CORS: explicit origins from VCS_CORS_ORIGINS when set (empty = deny all
|
|
38
|
+
# cross-origin — right for prod, where the SPA is same-origin). The
|
|
39
|
+
# any-localhost-port regex is a dev-only default; with credentials
|
|
40
|
+
# allowed it must not reach a real deployment.
|
|
41
|
+
origins = get_cors_origins()
|
|
42
|
+
cors_scope = (
|
|
43
|
+
{"allow_origins": origins}
|
|
44
|
+
if origins is not None
|
|
45
|
+
else {"allow_origin_regex": r"http://localhost:\d+"}
|
|
46
|
+
)
|
|
47
|
+
app.add_middleware(
|
|
48
|
+
CORSMiddleware,
|
|
49
|
+
allow_credentials=True,
|
|
50
|
+
allow_methods=["*"],
|
|
51
|
+
allow_headers=["*"],
|
|
52
|
+
**cors_scope,
|
|
53
|
+
)
|
|
54
|
+
app.include_router(health.router)
|
|
55
|
+
app.include_router(auth.router)
|
|
56
|
+
app.include_router(tokens.router)
|
|
57
|
+
app.include_router(groups.router)
|
|
58
|
+
app.include_router(ingest.router)
|
|
59
|
+
app.include_router(findings.router)
|
|
60
|
+
app.include_router(overview.router)
|
|
61
|
+
app.include_router(repos.router)
|
|
62
|
+
app.include_router(scans.router)
|
|
63
|
+
app.include_router(analytics.router)
|
|
64
|
+
_mount_dashboard(app)
|
|
65
|
+
return app
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _mount_dashboard(app: FastAPI) -> None:
|
|
69
|
+
"""Serve the built dashboard SPA from the API when bundled.
|
|
70
|
+
|
|
71
|
+
Active only when VCS_DASHBOARD_DIR points at a build (so tests/local
|
|
72
|
+
are unaffected). API routes are registered first and take precedence;
|
|
73
|
+
the catch-all serves real files, else index.html for client-side routes.
|
|
74
|
+
"""
|
|
75
|
+
dist = os.environ.get("VCS_DASHBOARD_DIR")
|
|
76
|
+
if not dist:
|
|
77
|
+
return
|
|
78
|
+
root = Path(dist)
|
|
79
|
+
index = root / "index.html"
|
|
80
|
+
if not index.is_file():
|
|
81
|
+
return
|
|
82
|
+
if (root / "assets").is_dir():
|
|
83
|
+
app.mount(
|
|
84
|
+
"/assets", StaticFiles(directory=root / "assets"), name="assets"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@app.get("/{full_path:path}", include_in_schema=False)
|
|
88
|
+
def spa(full_path: str) -> FileResponse:
|
|
89
|
+
candidate = root / full_path
|
|
90
|
+
if full_path and candidate.is_file():
|
|
91
|
+
return FileResponse(candidate)
|
|
92
|
+
return FileResponse(index)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Authentication principal and the global tenant-isolation safety net.
|
|
2
|
+
|
|
3
|
+
A single ``AuthPrincipal`` represents whoever is making a request — a machine
|
|
4
|
+
token (exactly one group) or, later, a human SSO session (their group
|
|
5
|
+
memberships). Every isolation rule is expressed once against
|
|
6
|
+
``accessible_group_ids`` so both auth modes share the same enforcement.
|
|
7
|
+
|
|
8
|
+
The isolation itself is a SQLAlchemy ``do_orm_execute`` listener: any session
|
|
9
|
+
whose ``info`` carries ``accessible_group_ids`` automatically has a
|
|
10
|
+
group-scoped predicate injected into every SELECT of ``Repo``/``Finding``/
|
|
11
|
+
``Scan``. This is a safety net — a forgotten ``.filter()`` in a route cannot
|
|
12
|
+
leak another group's data, because the predicate is applied at the session
|
|
13
|
+
layer, not by the route.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import uuid
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from sqlalchemy import event, select
|
|
22
|
+
from sqlalchemy.orm import Session, with_loader_criteria
|
|
23
|
+
|
|
24
|
+
SCOPE_KEY = "accessible_group_ids"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class AuthPrincipal:
|
|
29
|
+
"""The authenticated caller and the groups they may access."""
|
|
30
|
+
|
|
31
|
+
kind: str # "token" | "user"
|
|
32
|
+
org_id: uuid.UUID
|
|
33
|
+
group_ids: frozenset[uuid.UUID]
|
|
34
|
+
token_id: uuid.UUID | None = None
|
|
35
|
+
user_id: uuid.UUID | None = None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def accessible_group_ids(self) -> frozenset[uuid.UUID]:
|
|
39
|
+
return self.group_ids
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def primary_group_id(self) -> uuid.UUID:
|
|
43
|
+
"""The single group a write (ingest) is attributed to.
|
|
44
|
+
|
|
45
|
+
Tokens are scoped to exactly one group. Raising here surfaces a
|
|
46
|
+
misuse (e.g. attributing a write to a multi-group user session)
|
|
47
|
+
rather than silently picking one.
|
|
48
|
+
"""
|
|
49
|
+
if len(self.group_ids) != 1:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"primary_group_id requires exactly one accessible group"
|
|
52
|
+
)
|
|
53
|
+
return next(iter(self.group_ids))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def scope_session(session: Session, group_ids: frozenset[uuid.UUID]) -> None:
|
|
57
|
+
"""Bind a session to a principal's accessible groups for the request."""
|
|
58
|
+
session.info[SCOPE_KEY] = group_ids
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@event.listens_for(Session, "do_orm_execute")
|
|
62
|
+
def _apply_group_scope(execute_state) -> None:
|
|
63
|
+
"""Inject a group-scoped predicate on every scoped SELECT.
|
|
64
|
+
|
|
65
|
+
Skips relationship/column loads so navigating ``finding.repo`` and
|
|
66
|
+
similar lazy loads still work; only top-level entity SELECTs are filtered.
|
|
67
|
+
"""
|
|
68
|
+
if (
|
|
69
|
+
not execute_state.is_select
|
|
70
|
+
or execute_state.is_column_load
|
|
71
|
+
or execute_state.is_relationship_load
|
|
72
|
+
):
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
group_ids = execute_state.session.info.get(SCOPE_KEY)
|
|
76
|
+
if group_ids is None:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# Local imports avoid a circular import at module load time.
|
|
80
|
+
from vcs.models.finding import Finding
|
|
81
|
+
from vcs.models.repo import Repo
|
|
82
|
+
from vcs.models.scan import Scan
|
|
83
|
+
|
|
84
|
+
repo_ids = select(Repo.id).where(Repo.group_id.in_(group_ids))
|
|
85
|
+
execute_state.statement = execute_state.statement.options(
|
|
86
|
+
with_loader_criteria(
|
|
87
|
+
Repo, Repo.group_id.in_(group_ids), include_aliases=True
|
|
88
|
+
),
|
|
89
|
+
with_loader_criteria(
|
|
90
|
+
Finding, Finding.repo_id.in_(repo_ids), include_aliases=True
|
|
91
|
+
),
|
|
92
|
+
with_loader_criteria(
|
|
93
|
+
Scan, Scan.repo_id.in_(repo_ids), include_aliases=True
|
|
94
|
+
),
|
|
95
|
+
)
|