Skip to Content
ContributingSecurity

Security for Contributors

This page explains the security checks contributors see when they open a pull request. The canonical security policy is docs/SECURITY.md.

Do not report sensitive vulnerabilities in public issues, pull requests, or discussions. Email security reports to security@sundsvall.se.

Pull Request Checks

Pull requests into develop run the following security and quality gates:

CI

CI / CI is the main app check. It runs backend quality checks, backend tests, frontend checks, package lint, and the frontend production build.

Backend dependency installs use uv sync --frozen --all-groups. Frontend dependency installs use bun install --frozen-lockfile. This matters because dependency PRs should prove that the committed lockfiles still install and the app still builds.

The CI workflow also builds the backend, Svelte frontend, Next frontend, and devcontainer Docker images without pushing them. This catches Dockerfile and base-image update problems before Dependabot Docker PRs merge.

Ruff currently checks backend/src/eneo. Tests and Alembic are not part of the Ruff gate yet.

Dependency Review

Dependency Review / Dependency Review checks dependency and lockfile changes in pull requests. It fails if a PR introduces a known vulnerable dependency at high severity or above.

When this check fails, update the dependency to a safe version, remove it, or document why the vulnerable code is not reachable. Do not dismiss the warning just to make the PR green.

Source, dependency and secret scanning

The reusable Security workflow is part of the CI aggregate. It runs a complete lockfile audit, Semgrep’s security-audit and GitHub Actions rules, and Gitleaks on added Git history. The dependency audit blocks high and critical findings, including those without a fixed version. low and moderate findings produce warning annotations and remain in the reports. Python audit severities come from the GitHub Advisory Database; the fetched ratings are retained with the raw audit report. Unknown severity, failed scans and invalid reports still fail CI. Existing exact, unexpired exceptions remain valid; expired exceptions still require review and fail the check. Temporary dependency mitigations require a documented owner, targeted tests and an expiry; see frontend/patches/SECURITY.md.

The release review also scans full Git history, verifies credential revocation, and exercises both frontends in staging with the actual authentication and integration providers. Automated scanning does not replace those checks.

Code scanning

CodeQL advanced setup is paused while maintainers evaluate a code-scanning setup that gives developers useful feedback without high false-positive noise. Re-enable code scanning only with a tuned query set and a triage process that keeps alerts actionable.

Declare access on every endpoint

Every HTTP and WebSocket endpoint must explicitly declare authentication (which identity it accepts) and authorization (who may invoke the operation). There is no implicit “logged in is enough” default and no exemption based on a URL prefix. This implements the deny-by-default and per-request checking principles in the OWASP Authorization Cheat Sheet . It does not establish compliance with the whole OWASP ASVS standard.

Declare the policy immediately below the route decorator, and retain the real authentication dependency:

@router.post( "/{provider_id}/test/", response_model=ProviderTestResult, description="Test connectivity using the provider's stored credentials.", responses=responses.get_responses([403, 404]), ) @endpoint_access( authentication=Authentication.USER, authorization=Permission.ADMIN, reason="Uses organization-managed provider credentials.", ) async def test_provider(provider_id: UUID, user: CurrentUser): ...

The canonical declaration and enforcement live in backend/src/eneo/authentication/endpoint_access.py. Existing Permission values and domain services remain the owners of role permissions, tenant boundaries, space membership and object ownership. Endpoint admission never replaces those resource checks. Use a named permission whenever the operation requires one. Authorization.AUTHENTICATED is an explicit grant to authenticated callers, suitable for a shared catalogue or an operation whose service restricts access to the caller’s own resources; its reason must identify that purpose. When several routes rely on the same access rule, reuse a router-local named constant for their reason. Keep the full @endpoint_access declaration on each route, and use separate reasons when the underlying checks differ.

The declaration is the single source for the endpoint’s permission and session requirements. Do not repeat these with validate_permission in handlers or separate permission/session dependencies. Domain service permission checks remain necessary for internal callers. Storage administration also retains its stricter active-user and active-tenant checks; these protect identity lifecycle, independently of the declared Permission.STORAGE.

Choose Authentication.SESSION when API keys must be rejected. The existing API-key scope, permission and real-user guards still apply. SCIM tokens, deployment administrator keys, module credentials and signed file URLs have separate policies and authentication dependencies. Public operations such as login entry points and health probes must declare both Authentication.PUBLIC and Authorization.PUBLIC, with a reason.

Application construction checks every registered route, including nested routers and mounted APIs. A missing declaration or missing authentication dependency prevents startup. The request guard also rejects missing or misconfigured access contracts, and user authentication enforces the declared permission before the handler executes. Generated documentation and internal MCP transport routes have explicit declarations at their registration sites; MCP tools still authorize each tool invocation.

scripts/check_route_metadata.py --all checks declarations and rejects duplicate endpoint permission/session gates without importing the application. CI also runs tests/unit/test_endpoint_access.py and tests/unit/test_endpoint_access_matrix.py. The matrix exercises the shipped user-authentication dependency graphs, rejecting anonymous callers, users missing the declared permission and API keys on session-only endpoints before handler dispatch. When adding an endpoint, add behavior tests showing that anonymous callers and callers without the required permission cannot reach its side effects, while an authorized caller succeeds. For resource operations, test another user’s or tenant’s resource as well. A declaration cannot prove that the chosen permission or domain rule is correct.

The request adapter reads FastAPI’s effective dependency context so inherited authentication is included. Its scope layout is framework-specific; retain the nested-router and mounted-API tests when upgrading FastAPI.

Required Checks in Branch Protection

The develop ruleset should require these checks:

  • CI / CI
  • Dependency Review / Dependency Review

Remove older or paused required status checks if they appear as Expected and never report:

  • CodeQL gate
  • Backend checks (Ruff)
  • Frontend basic checks
  • Run unit tests
  • Run integration tests
  • Test Docker builds

Those names belonged to the previous PR workflows. They are not queued work; GitHub is waiting for checks that no longer exist on this branch.

The old Test Docker builds check is replaced by Docker build subjobs inside CI / CI.

Dependency Updates

Dependabot opens weekly PRs for:

  • backend Python dependencies in backend
  • frontend Bun dependencies in frontend
  • GitHub Actions
  • Dockerfiles
  • devcontainer configuration

Routine minor and patch updates are grouped to reduce PR noise. Do not enable auto-merge until CI and Dependency Review have been stable for long enough that failures are rare and understandable.

For packages that handle prompts, files, provider keys, network calls, authentication, authorization, Docker images, or GitHub Actions, read the changelog before merging.

Handling Alerts

Use this order when triaging security alerts:

  1. Same day: malware, exposed secrets, critical runtime vulnerabilities, authentication or authorization issues, file parsing issues, SSRF, RCE, deserialization, and LLM provider credential exposure.
  2. Within 48-72 hours: high runtime vulnerabilities, vulnerable production Docker base images, and vulnerable GitHub Actions in trusted workflows.
  3. Within 1-2 weeks: medium runtime vulnerabilities and critical or high build-time vulnerabilities.
  4. Monthly: low severity findings, dev-only issues without a practical exploit path, and major version update backlog.

If a fix cannot be merged quickly, create a GitHub issue or security tracking item with an owner, risk, mitigation, target date, and review date.

Secrets

Use GitHub Actions secrets for CI and deployment credentials. Use Dependabot secrets only for private package registries. Do not put runtime application secrets in Dependabot secrets.

If a secret enters the repository, rotate it first. Then remove it from history if needed, inspect logs and artifacts, and add a secret scanning pattern if the format is specific to Eneo.