Contributing¶
Development environment¶
uv sync --extra dev
uv run pytest
uv run ruff check . && uv run ruff format --check .
pre-commit install # optional: same checks before every commit, and the dependency
# scan when SECREVIEW_MCP_URL is set
The prettier hook formats the YAML and JSON files and the sources of web/src/ with the
Prettier version locked by web/package-lock.json (the one of CI): run npm ci in web/
before the first commit. CI checks the same files (lint.yml, web job). .prettierrc sets
the width to 100 columns, like ruff; lock files are listed in .prettierignore.
Security review of each commit¶
The optional agent-review hook (scripts/agent-review.sh) asks Claude Code to review each of
your commits for security, in the background, with the security-review MCP server of your
.mcp.json. The review follows the security review procedure of AGENTS.md and saves
one report, which you read in the web interface. Enable it once:
prek install --hook-type post-commit
SKIP=agent-review git commit ... # skip it for one commit
- The commit is never blocked: the review runs after it, and its log is in
.git/agent-review/<commit>.log. - The agent receives the commit in its prompt and may only read the repository and call the
review tools of the server (
--restricted,--permission-mode dontAsk): it edits no file, runs no command, proposes no finding status and cannot read.env,.mcp.jsonor any file outside the repository. - The hook does nothing for a commit made by Claude Code (already reviewed in its session),
during a rebase or a cherry-pick, or when the
claudeCLI or.mcp.jsonis missing. - Each review consumes model usage on your Claude account.
In VS Code (.vscode/):
launch.json configuration |
Role |
|---|---|
| MCP server + controller | Runs the Database upgrade task, then both processes on the same database |
| MCP server (HTTP, port 8000) | MCP server only; the database must already be migrated |
| Controller (web UI and API, port 8080) | Controller only; migrates the database and shows the password of the initial admin account at first start |
| Controller CLI (prompt for arguments) | A controller command (db status, token list…) under the debugger |
| Controller + web UI (Angular, port 4200) | Controller and Angular application under the debugger, hot reload |
| Web UI + API (Angular dev server, port 4200) | Runs the Web UI + API: dev servers task (controller on 8080 and ng serve), waits for them to be ready, then opens Chrome on http://localhost:4200/ui/ with the TypeScript breakpoints |
| Pytest (current file) | Tests of the open file |
tasks.json task |
Command |
|---|---|
| Build security_review_mcp | uv sync --extra dev --extra docs --extra postgres |
| Database upgrade | uv run security-review-controller db upgrade (after Build) |
| Tests / Lint | pytest, then ruff and mkdocs build --strict |
| Web UI: build / Web UI: dev server (port 4200) | Angular build served by the controller, or ng serve with /api forwarded to port 8080 |
| API: controller (port 8080) / Web UI + API: dev servers | Controller only outside the debugger (security-review-controller serve), or controller and ng serve in parallel |
| Docs: serve (port 8001) | mkdocs serve |
| Docker: compose up / down | docker-compose.yml stack of the image, with its own PostgreSQL database (same ports 8000 and 8080: stop the debug configurations first) |
Devcontainer¶
The repository provides a devcontainer (.devcontainer/) with Python, uv, Node.js, a dedicated
Docker daemon (docker-in-docker feature) and a PostgreSQL 17 database. compose.yaml
describes two services: app, the development container, and db, the database, reachable
from app under the db name and kept in the secreview-dev-pg volume between two rebuilds.
SECREVIEW_DATABASE_URL points to it by default: the MCP server, the controller and the CLI use
PostgreSQL. The tests always use an in-memory SQLite database, and ignore your .env file and
the SECREVIEW_* variables of the shell.
At creation, the devcontainer installs the psql client, the prek hooks and the full
environment (uv sync --extra dev --extra docs --extra postgres), then migrates the database. It
forwards ports 8000 (MCP server), 8080 (controller), 4200 (Angular dev server), 8001 (docs) and
5432 (PostgreSQL, development credentials secreview / secreview).
psql -h db -U secreview secreview # password: secreview
SECREVIEW_DATABASE_URL=sqlite:///./dev.db security-review-mcp serve # back to SQLite for once
The Docker daemon lets you build and run the image of the Dockerfile without leaving the container:
docker build -t security-review-mcp .
docker run --rm -p 8080:8000 -v secreview-data:/var/lib/secreview \
--entrypoint security-review-controller security-review-mcp serve --host 0.0.0.0
docker run --rm -p 8000:8000 -v secreview-data:/var/lib/secreview security-review-mcp \
serve --transport streamable-http --host 0.0.0.0 --port 8000
The ports are forwarded to the workstation: the web interface is on http://localhost:8080/ui/,
the REST API on /api/v1 and the MCP endpoint on http://localhost:8000/mcp. Images and
volumes are kept in volumes of the devcontainer: they do not mix with those of the host.
Why not the Docker socket of the host?
Mounting /var/run/docker.sock would give every tool of the devcontainer, AI agents
included, root-equivalent access to the host. The dedicated daemon requires a privileged
container, but stays isolated from the daemon of the host. After adding the feature, rebuild
the container (Dev Containers: Rebuild Container).
Definition of done¶
A contribution, human or produced by an AI agent, is only done if the behavior it adds or fixes
is checked by a relevant test, new or existing, the docs/ documentation is up to date in both languages and the README.md files, which stay summaries,
are too. When the security-review MCP server is up, the changes are also reviewed with it
(the project reviews itself) and the report is saved. The detailed rules (conventions, security
invariants, mapping from code to doc pages) are in AGENTS.md at the root of the repository;
CLAUDE.md and .github/copilot-instructions.md point to it.
A test protects a contract or an invariant, not a line of code, and there is no coverage
target. Test first the security invariants, the public contracts (MCP tools and prompts, CLI exit
codes, REST schema, migrations) and each fixed bug (one regression test). Do not test what a
library already guarantees, implementation details or the exact text of a message. Before adding
a test, extend the one that already covers the behavior (extra assertion, parametrize row): a
change usually adds one to three tests.
Code layout¶
src/security_review_mcp/
├── server.py # prompts, tools, resources, entry point of the MCP server
├── controller.py # entry point of the controller (serve, db and administration commands)
├── http_server.py # HTTP of both processes: Bearer tokens and roles, TLS, /healthz, uvicorn
├── oidc.py # OpenID Connect OIDC login to the web interface (controller)
├── access_control.py # permissions, roles, groups and members of the web accounts, project enrollment
├── token_store.py # Bearer tokens in the database: creation, expiry, rotation, revocation
├── audit.py # audit log: events of triage decisions, proposals, tokens and enrollments
├── audit_syslog.py # forwarding of the audit events to syslog by the controller (optional)
├── metrics.py # Prometheus metrics of the controller (/metrics)
├── web_users.py # web interface accounts, local (scrypt) or OIDC (created on the fly), sessions, initial admin
├── enrollment.py # `enroll` client: project enrollment on a shared server
├── remote_scan.py # `scan-deps` client: dependency scan with a shared server
├── ci_client.py # `security-review-ci`: pipeline client of the controller, file listing (standard library only)
├── enrollment_api.py # public POST /api/v1/enroll endpoint of the MCP server
├── web_api/ # controller: REST API /api/v1 and files of the application (/ui)
│ ├── __init__.py # create_app, static files of /ui, assembly of the routers
│ ├── schemas.py # request and response models of every route
│ ├── deps.py # identity of the caller, permissions, project scope
│ └── *_routes.py # one router per domain (auth, account, report, ci, rule, token, admin, access)
├── prompts.py # construction of the prompts
├── standards.py # development standards: storage, lookup by technology / file, rendering
├── default_rules.toml # default rules and checklists, copied into the database at first start
├── checklists.py # structure of a checklist item
├── scanner.py # scan engine, inline suppression, secret masking
├── syntax_analysis.py # Tree-sitter structural checks, separate from line-based rules
├── rules_loader.py # rule validation, reading of the TOML packs and of the default file
├── rule_store.py # rules in the database: seeding, import/export, enabling, deletion
├── registry.py # active rules/checklists (read from the database), reload
├── cli.py # db, advisories, rules, token, audit, enroll, scan-deps commands
├── codescan.py # whole codebase scan of the files sent by security-review-ci (findings)
├── sarif.py # SARIF 2.1.0 export
├── trends.py # trend report
├── versions.py # version comparison
├── update_check.py # search for a newer release on GitHub (About page, optional)
├── ticketing/ # tickets of the findings: api (connector API), registry, connections,
│ # transport (HTTP client), core (sending), jira, servicenow
├── healthcheck.py # HEALTHCHECK of the Docker image (/healthz of the HTTP processes)
├── advisories.py # OSV import, version matching
├── advisory_sync.py # OSV download by the controller (proxy, daily update, history)
├── app_settings.py # settings changed in the interface (language, outgoing proxy, OIDC), in the database
├── proxy.py # outgoing proxy of the controller (OSV, OIDC): URL, no_proxy, urllib handler
├── dependencies.py # manifest / lockfile parsers, hygiene
├── depscan.py # dependency scan and conversion into findings
├── services.py # persistence logic of the reports
├── coverage.py # checks the checklist results and scan rules declared with a report
├── project_names.py # normalization of the project names (every input)
├── retention.py # daily purge of the reports without findings and of the old audit events
├── models.py # SQLModel models (tables + schemas)
├── database.py # engine, sessions and migrations (Alembic)
├── migrations/ # Alembic environment and schema revisions (versions/)
└── config.py # settings (SECREVIEW_*)
tests/ # pytest (scanner, services, end-to-end MCP client)
web/ # Angular application (built into src/security_review_mcp/webui/)
Web interface¶
The Angular 22 application (signals, zoneless, Bootstrap 5) is in web/. Its build is written
into src/security_review_mcp/webui/, from where the controller serves it on /ui/ and the
wheel embeds it.
cd web
npm ci
npm run build # production build (to redo before uv build)
npm start # dev server on http://localhost:4200/ui/
npm run format # Prettier
npm start forwards /api to http://127.0.0.1:8080 (web/proxy.conf.json): start the
controller in parallel with security-review-controller serve --port 8080, or use the VS Code
configuration Web UI + API (Angular dev server, port 4200), which starts both
(Controller + web UI (Angular, port 4200) if you also want to debug the Python): every save
of a file of web/src/ reloads the page (styles and templates without a full reload), and the
breakpoints set in the .ts files are honored. In the devcontainer, Chrome is opened on the
local workstation and port 4200 is forwarded automatically.
Code conventions¶
- REST API: every route that answers JSON declares its response model (
web_api/schemas.py): FastAPI documents it in OpenAPI and drops any field that is not declared.test_openapi_documents_the_response_of_every_json_routefails on a route that returns a baredict. - Components: template and styles in separate files. A feature with several screens has one
folder per component (
features/access/role-list/); its shared files (styles, base class, error messages) stay at the root of the feature. - Forms: the input forms use the Signal Forms (
form(),[formField], validators with a translated message); the list filters and the dynamic lists of checkboxes stay plain event handlers. - HTTP calls: through
rxResourcefor the data of a screen, orawait firstValueFrom(...)for an action; no.subscribe()in the components. - State:
signal()andcomputed(); a value derived from another one but still editable (a form filled with the loaded settings) is alinkedSignal().effect()is kept for real side effects (document attributes, local storage, timers), never to copy a signal into another. A service exposes its signals withasReadonly()and changes them through named methods. - Folders:
shared/holds reusable components without business logic: they get their data through inputs and report through outputs, never through a business service ofcore/(the finding card emits the decision,TriageServicesaves it). The shell of the application islayout/. - Templates: host bindings in
host: {}(no@HostListener); a message that explains why a field is invalid is linked to it witharia-describedby. - Tables:
app-table-controlsholds the search and the filters; the sort is chosen on the column headers (<th [appSortHeader]="sort" sortKey="name">), with aTableSortnamed after the table (shared/utils/table-sort.ts) that the browser remembers undersecreview.sort.<name>. - Colors: Bootstrap variables (
var(--bs-…)) or the design tokens ofstyles.css(--app-…), never a color written in a component, so that the light and dark themes work.
Interface translations¶
The interface is translated into English, French and Spanish by an in-house service
(web/src/app/core/i18n/), without any extra dependency. Each screen has its catalog in
core/i18n/messages/<screen>.ts, declared in messages/index.ts, with its messages in the three
languages:
export const tokens = defineMessages({
en: { 'tokens.created': 'Token “{label}” created.' },
fr: { 'tokens.created': 'Jeton « {label} » créé.' },
es: { 'tokens.created': 'Token «{label}» creado.' },
});
- In a template:
{{ 'tokens.title' | t }}, or with parameters{{ 'tokens.created' | t: { label: name } }}. - In the code:
I18nService.translate(...)(confirmations, messages of the Signal Forms validators throughmessage: () => ...). A message shown later (success, error) is kept asmsg(key, parameters)and translated in the template, so that it follows a change of language. - The title of the routes is a key, translated by
I18nTitleStrategy.
The keys are typed: an unknown key, or a key missing in one of the three languages, fails
npm run build.
Schema migrations¶
Every change of a table model (models.py) comes with an Alembic revision in
src/security_review_mcp/migrations/versions/. The
test_upgrade_creates_the_schema_of_the_models test (tests/test_database.py) fails as long
as the migrations do not produce exactly the schema of the models. To generate a draft
revision to review:
uv run python - <<'PY'
from alembic import command
from security_review_mcp import database
with database.get_engine().begin() as connection:
command.revision(
database.alembic_config(connection), "describe the change", autogenerate=True, rev_id="0002"
)
PY
Run it with SECREVIEW_DATABASE_URL pointing to a throwaway SQLite database already up to date
(security-review-controller db upgrade). Number the revisions in sequence (0002, 0003…),
review the generated file and never change a revision already published.
Documentation¶
The documentation is written in Markdown in docs/, in English and French, and built with
MkDocs Material and the
mkdocs-static-i18n plugin.
uv sync --extra docs
uv run mkdocs serve # preview on http://127.0.0.1:8000, hot reload
uv run mkdocs build --strict # what the CI does: a broken link fails the build
- Every page exists twice, side by side:
page.mdin English (default language, served at the root of the site) andpage.fr.mdin French (served under/fr/). Change both in the same commit. - Links between pages target the English file (
../reference/cli.md): the plugin points them to the French page from a French page. Anchors follow the headings of each language (#report-languagein English,#langue-des-rapportsin French). - The pages follow the order of a set-up: Getting started (
getting-started/) and Usage (usage/) only hold what is needed to work; Advanced (advanced/) gathers the details and the optional features. Put a detailed explanation inadvanced/, with at most a line and a link from the essential pages. - To add a page, create both files in
docs/, declare the English one in thenavsection ofmkdocs.yml, and translate its title innav_translations(French language of thei18nplugin).
Publishing¶
The .github/workflows/docs.yml workflow:
- on a pull request that touches
docs/ormkdocs.yml: builds the site in--strictmode (check only); - on a push on
master: builds the site and publishes it on GitHub Pages.
One-time activation in the GitHub repository: Settings → Pages → Build and deployment → Source: GitHub Actions.
Branches¶
dev: development branch, the default place to code.feat/<topic>: only for a large break (rework, API or schema change), created fromdevand merged intodevby pull request.master: production branch, only fed by merges ofdev.
Releasing a version¶
A version is a tag pushed on master:
- On
dev, updateversioninpyproject.toml(e.g.uv version 1.3.0) and commit. - Merge
devintomaster. - On
master, push the matching tag:git tag v1.3.0 && git push origin v1.3.0. - The push of the tag triggers
publish.yml: lint and tests, check that the tag matches the version, publication on PyPI and of the Docker images (docker.yml), then creation of the GitHub release.
The .github/workflows/docker.yml workflow builds the image (linux/amd64, linux/arm64,
default and -pg variants) and checks that both entry points start. On every pull request
(the protection of master requires these checks), it only builds; on a version tag, it pushes the
images to ghcr.io/cyr-ius/security-review-mcp and docker.io/cyrius44/security-review-mcp
(see published images). A manual run (Actions →
Docker image → Run workflow, push checked) publishes a branch image (dev, dev-pg).
One-time setup: a Docker Hub access token (read and write) in the repository secret
DOCKERHUB_TOKEN; GHCR uses the token of the workflow. After the first publication, make the
GHCR package public (Packages → security-review-mcp → Package settings).
Every action of the workflows is pinned to a full commit SHA, with its version in a comment
(uses: actions/checkout@<40-character SHA> # v7.0.1): a tag can be moved, a commit cannot.
Dependabot (.github/dependabot.yml) updates the SHA and the comment together. Pin any new
action the same way.