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.
Files changed (60) hide show
  1. vehlo_code_scanner-0.1.1rc1/.gitignore +53 -0
  2. vehlo_code_scanner-0.1.1rc1/PKG-INFO +198 -0
  3. vehlo_code_scanner-0.1.1rc1/README.md +170 -0
  4. vehlo_code_scanner-0.1.1rc1/pyproject.toml +81 -0
  5. vehlo_code_scanner-0.1.1rc1/src/vcs/__init__.py +1 -0
  6. vehlo_code_scanner-0.1.1rc1/src/vcs/api/__init__.py +1 -0
  7. vehlo_code_scanner-0.1.1rc1/src/vcs/api/app.py +92 -0
  8. vehlo_code_scanner-0.1.1rc1/src/vcs/api/auth.py +95 -0
  9. vehlo_code_scanner-0.1.1rc1/src/vcs/api/deps.py +161 -0
  10. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/__init__.py +1 -0
  11. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/analytics.py +79 -0
  12. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/auth.py +169 -0
  13. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/findings.py +141 -0
  14. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/groups.py +122 -0
  15. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/health.py +11 -0
  16. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/ingest.py +49 -0
  17. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/overview.py +95 -0
  18. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/repos.py +106 -0
  19. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/scans.py +79 -0
  20. vehlo_code_scanner-0.1.1rc1/src/vcs/api/routes/tokens.py +178 -0
  21. vehlo_code_scanner-0.1.1rc1/src/vcs/api/schemas.py +172 -0
  22. vehlo_code_scanner-0.1.1rc1/src/vcs/api/services/__init__.py +1 -0
  23. vehlo_code_scanner-0.1.1rc1/src/vcs/api/services/auto_resolve.py +55 -0
  24. vehlo_code_scanner-0.1.1rc1/src/vcs/api/services/ingest.py +205 -0
  25. vehlo_code_scanner-0.1.1rc1/src/vcs/api/session.py +32 -0
  26. vehlo_code_scanner-0.1.1rc1/src/vcs/cli/__init__.py +1 -0
  27. vehlo_code_scanner-0.1.1rc1/src/vcs/cli/admin.py +177 -0
  28. vehlo_code_scanner-0.1.1rc1/src/vcs/cli/app.py +19 -0
  29. vehlo_code_scanner-0.1.1rc1/src/vcs/cli/output.py +68 -0
  30. vehlo_code_scanner-0.1.1rc1/src/vcs/cli/scan_command.py +195 -0
  31. vehlo_code_scanner-0.1.1rc1/src/vcs/client/__init__.py +1 -0
  32. vehlo_code_scanner-0.1.1rc1/src/vcs/client/api.py +108 -0
  33. vehlo_code_scanner-0.1.1rc1/src/vcs/config.py +150 -0
  34. vehlo_code_scanner-0.1.1rc1/src/vcs/db.py +47 -0
  35. vehlo_code_scanner-0.1.1rc1/src/vcs/enums.py +55 -0
  36. vehlo_code_scanner-0.1.1rc1/src/vcs/fingerprint.py +17 -0
  37. vehlo_code_scanner-0.1.1rc1/src/vcs/models/__init__.py +22 -0
  38. vehlo_code_scanner-0.1.1rc1/src/vcs/models/api_token.py +39 -0
  39. vehlo_code_scanner-0.1.1rc1/src/vcs/models/base.py +32 -0
  40. vehlo_code_scanner-0.1.1rc1/src/vcs/models/finding.py +81 -0
  41. vehlo_code_scanner-0.1.1rc1/src/vcs/models/finding_history.py +42 -0
  42. vehlo_code_scanner-0.1.1rc1/src/vcs/models/group.py +35 -0
  43. vehlo_code_scanner-0.1.1rc1/src/vcs/models/organization.py +23 -0
  44. vehlo_code_scanner-0.1.1rc1/src/vcs/models/repo.py +56 -0
  45. vehlo_code_scanner-0.1.1rc1/src/vcs/models/scan.py +62 -0
  46. vehlo_code_scanner-0.1.1rc1/src/vcs/models/user.py +53 -0
  47. vehlo_code_scanner-0.1.1rc1/src/vcs/scanner/__init__.py +1 -0
  48. vehlo_code_scanner-0.1.1rc1/src/vcs/scanner/models.py +67 -0
  49. vehlo_code_scanner-0.1.1rc1/src/vcs/scanner/parser.py +25 -0
  50. vehlo_code_scanner-0.1.1rc1/src/vcs/scanner/runner.py +146 -0
  51. vehlo_code_scanner-0.1.1rc1/src/vcs/services/__init__.py +1 -0
  52. vehlo_code_scanner-0.1.1rc1/src/vcs/services/provisioning.py +144 -0
  53. vehlo_code_scanner-0.1.1rc1/src/vcs/services/tokens.py +74 -0
  54. vehlo_code_scanner-0.1.1rc1/src/vcs/storage/__init__.py +55 -0
  55. vehlo_code_scanner-0.1.1rc1/src/vcs/storage/memory.py +19 -0
  56. vehlo_code_scanner-0.1.1rc1/src/vcs/storage/s3.py +49 -0
  57. vehlo_code_scanner-0.1.1rc1/src/vcs/worker/__init__.py +1 -0
  58. vehlo_code_scanner-0.1.1rc1/src/vcs/worker/celery_app.py +17 -0
  59. vehlo_code_scanner-0.1.1rc1/src/vcs/worker/db.py +33 -0
  60. 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
+ )