Skip to content

Commit 3239f27

Browse files
fabriziosalmiclaude
andcommitted
v2.5.4 -- Tier 2 + Tier 3 closure: auth hardening, GDPR UX, OSS hygiene
TIER 2 -- security hardening on auth + GDPR-aware UX * /auth/forgot-password is now constant-time and PII-free in logs. Pre-2.5.4 the unknown-email path early-returned (5-20x faster than the known-email path) and the MTA-failure log printed user.email. Both paths now do the same token-gen + hash work and sleep a randomised 50-250ms; failure log records user.id only. * CSRF token rotation on /auth/refresh -- regression-locked. The rotation was already happening (every _build_token_response mints a fresh secrets.token_urlsafe(32)) but the invariant was nowhere asserted. Added inline rationale + TestCSRF.test_csrf_token_ rotates_on_refresh in the integration suite. * Findings page: dismissible GDPR PII notice banner. Scan evidence can include hostnames, employee emails, IPs -- all PII under Art. 4(1). Banner reminds the operator they're the controller for this data. 3 keys x 5 locales = 15 new i18n entries, parity 5/5. * docs/privacy.md v1.0 -> v1.1: rewrote the self-hosted section with concrete data-flow disclosures (data-inventory table, IP/UA in audit_logs + Art. 89 pseudonymisation on erasure, crt.sh as a third-party transfer, scanner-PII surfaces, deployer obligations). TIER 3 -- OSS hygiene * SECURITY.md fully rewritten: RFC 9116 cross-ref, GitHub-private- disclosure preferred, 24h/48h/5d/10d response-by-severity SLA, 35d medium-severity patch ceiling (CRA Reg. EU 2024/2847 conventions), 90d coordinated disclosure default, in/out-of-scope matrix, full enumerated security-measures section, hall-of-fame placeholder, PGP placeholder until permanent key is provisioned. * packages/web/public/.well-known/security.txt -- RFC 9116-compliant with Contact, Policy, Languages (en, it), Canonical, Expires 2027-04-30. * CODE_OF_CONDUCT.md -- Contributor Covenant 2.1. * .github/ISSUE_TEMPLATE/{bug_report,feature_request}.yml + config.yml. blank_issues_enabled: false; contact_links redirect security to private advisories, questions to Discussions, dual- license inquiries to email. * .github/PULL_REQUEST_TEMPLATE.md -- summary / why / surface impact / verification / NIS2-GDPR-legal exposure prompt. * .github/CODEOWNERS -- single-maintainer fallback with explicit re-listing of the security-sensitive paths. Misc: * .gitignore: .ruff_cache/ added. * .gitleaks.toml: nis2-findings-pii-notice-v\d+ added to allowlist. * API_VERSION 2.5.3 -> 2.5.4. Verified: * Web build green (24/24 pages). * 5/5 locales validate; findings.piiNotice* aligned. * security.txt linted by hand against RFC 9116 sec. 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f1c01c5 commit 3239f27

25 files changed

Lines changed: 851 additions & 59 deletions

.github/CODEOWNERS

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright (c) 2026 Fabrizio Salmi <fabrizio.salmi@gmail.com>
2+
# SPDX-License-Identifier: AGPL-3.0-only
3+
# NIS2 Compliance Platform — https://github.com/fabriziosalmi/nis2-public
4+
#
5+
# CODEOWNERS controls auto-assignment of reviewers on pull requests.
6+
# Today the project has a single maintainer, so every path falls back
7+
# to the same owner. The file still earns its keep:
8+
# - it makes the ownership explicit for any future fork or
9+
# dual-maintained scenario (just add lines, no schema change);
10+
# - it auto-requests review on every PR so nothing slips through
11+
# waiting for the author to remember to assign;
12+
# - GitHub uses it for required-review branch protection rules
13+
# even when there is only one owner.
14+
#
15+
# Syntax: https://docs.github.com/en/repositories/managing-your-repositories-settings-and-features/customizing-your-repository/about-code-owners
16+
17+
# Default — every path not matched by a more specific rule below.
18+
* @fabriziosalmi
19+
20+
# Security-sensitive surfaces. Listing them again is redundant while
21+
# there is one owner, but it documents the surfaces that warrant the
22+
# strictest review when ownership grows.
23+
SECURITY.md @fabriziosalmi
24+
.well-known/ @fabriziosalmi
25+
packages/web/public/.well-known/ @fabriziosalmi
26+
packages/api/app/middleware/csrf.py @fabriziosalmi
27+
packages/api/app/database.py @fabriziosalmi
28+
packages/api/app/routers/auth.py @fabriziosalmi
29+
packages/api/app/utils/target_validator.py @fabriziosalmi
30+
packages/api/alembic/ @fabriziosalmi
31+
infra/ @fabriziosalmi
32+
33+
# Legal / compliance copy. Any change here should ship in lockstep
34+
# with the dashboard's compliance page and the CHANGELOG.
35+
docs/privacy.md @fabriziosalmi
36+
docs/terms.md @fabriziosalmi
37+
LICENSE @fabriziosalmi
38+
CODE_OF_CONDUCT.md @fabriziosalmi
39+
40+
# CI / supply chain. A bad workflow change can leak secrets or
41+
# downgrade the security gates — explicit ownership prompts a
42+
# closer review.
43+
.github/workflows/ @fabriziosalmi
44+
.github/dependabot.yml @fabriziosalmi
45+
.gitleaks.toml @fabriziosalmi
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright (c) 2026 Fabrizio Salmi <fabrizio.salmi@gmail.com>
2+
# SPDX-License-Identifier: AGPL-3.0-only
3+
4+
name: Bug report
5+
description: Something works incorrectly or breaks. Not for security issues — see SECURITY.md.
6+
title: "[bug] "
7+
labels: ["bug", "triage"]
8+
body:
9+
- type: markdown
10+
attributes:
11+
value: |
12+
Thanks for taking the time to file a bug report. The platform is
13+
AGPL-3.0 *as is, without warranty* — but well-formed reports get
14+
addressed faster, so a few specifics here go a long way.
15+
16+
**Not a bug?** Use [Discussions](https://github.com/fabriziosalmi/nis2-public/discussions) for questions or design conversations.
17+
**Security issue?** Use the [private advisory flow](https://github.com/fabriziosalmi/nis2-public/security/advisories/new), not this template.
18+
19+
- type: input
20+
id: version
21+
attributes:
22+
label: Platform version
23+
description: |
24+
Run `cat packages/api/pyproject.toml | grep ^version` or check the bottom-left of the dashboard. If you can, also include the commit SHA (`git rev-parse HEAD`).
25+
placeholder: e.g. 2.5.4 (commit abc1234)
26+
validations:
27+
required: true
28+
29+
- type: dropdown
30+
id: deployment
31+
attributes:
32+
label: How are you running the platform?
33+
options:
34+
- "make dev (local Docker compose)"
35+
- "make prod (Caddy + production compose)"
36+
- "Custom deployment (Kubernetes / cloud / other)"
37+
- "Not running yet — bug in the install path"
38+
validations:
39+
required: true
40+
41+
- type: dropdown
42+
id: os
43+
attributes:
44+
label: Host OS
45+
options:
46+
- macOS (Apple Silicon)
47+
- macOS (Intel)
48+
- Linux (Debian / Ubuntu)
49+
- Linux (RHEL / Fedora / Rocky / Alma)
50+
- Linux (other distro)
51+
- Windows + WSL2
52+
- Windows native (cmd.exe / PowerShell)
53+
- Other
54+
validations:
55+
required: true
56+
57+
- type: textarea
58+
id: what-happened
59+
attributes:
60+
label: What happened?
61+
description: A clear and concise description of the bug. What did you see versus what you expected?
62+
placeholder: |
63+
I clicked X and got Y, but I expected Z because the docs said …
64+
validations:
65+
required: true
66+
67+
- type: textarea
68+
id: reproduce
69+
attributes:
70+
label: Steps to reproduce
71+
description: Minimal repro from a fresh `make dev` clone whenever possible. Concrete commands beat prose.
72+
placeholder: |
73+
1. `git clone … && make dev`
74+
2. Register at /register with …
75+
3. Click …
76+
4. Observe …
77+
validations:
78+
required: true
79+
80+
- type: textarea
81+
id: logs
82+
attributes:
83+
label: Relevant logs
84+
description: |
85+
Output from `make api-logs`, `make web-logs`, browser DevTools console — whatever helps. **Strip any production secrets, real customer data, or internal hostnames before pasting.**
86+
render: shell
87+
validations:
88+
required: false
89+
90+
- type: checkboxes
91+
id: prereqs
92+
attributes:
93+
label: Pre-flight
94+
options:
95+
- label: I confirmed this is not a security vulnerability (security goes via the private advisory flow).
96+
required: true
97+
- label: I searched existing issues and discussions before opening this.
98+
required: true

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) 2026 Fabrizio Salmi <fabrizio.salmi@gmail.com>
2+
# SPDX-License-Identifier: AGPL-3.0-only
3+
# NIS2 Compliance Platform — https://github.com/fabriziosalmi/nis2-public
4+
#
5+
# `blank_issues_enabled: false` forces every new issue through one of
6+
# the structured templates below — saves a triage round on "what version
7+
# are you on", "is this a bug or a question", etc.
8+
9+
blank_issues_enabled: false
10+
contact_links:
11+
- name: Security vulnerability
12+
url: https://github.com/fabriziosalmi/nis2-public/security/advisories/new
13+
about: |
14+
Please use GitHub's private vulnerability reporting flow. Public
15+
issues for security bugs leave every operator running this code
16+
exposed before a fix is available — see SECURITY.md for the full
17+
policy and SLA.
18+
- name: Question / discussion
19+
url: https://github.com/fabriziosalmi/nis2-public/discussions
20+
about: |
21+
For questions, configuration help, or open-ended design
22+
discussions, please open a Discussion instead of an Issue.
23+
Issues are reserved for actionable bugs and feature requests.
24+
- name: Commercial / dual-license inquiry
25+
url: mailto:fabrizio.salmi@gmail.com
26+
about: |
27+
Email the maintainer directly for dual-license / consultancy
28+
requests. The codebase is AGPL-3.0; commercial-friendly licensing
29+
is available on a case-by-case basis.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright (c) 2026 Fabrizio Salmi <fabrizio.salmi@gmail.com>
2+
# SPDX-License-Identifier: AGPL-3.0-only
3+
4+
name: Feature request
5+
description: Suggest a new capability or an enhancement to an existing feature.
6+
title: "[feat] "
7+
labels: ["enhancement", "triage"]
8+
body:
9+
- type: markdown
10+
attributes:
11+
value: |
12+
Thanks for thinking about how the platform could be better. The bar for accepting a feature request is "this helps a real NIS2 / Art. 21 / Art. 18 use case for an Italian or EU operator", so the framing below is what we'll triage against.
13+
14+
- type: textarea
15+
id: problem
16+
attributes:
17+
label: What problem are you trying to solve?
18+
description: Lead with the pain, not the proposed solution. "I can't see X" is more useful than "Add a new tab".
19+
placeholder: |
20+
When I run a scan against …, I cannot tell whether …
21+
validations:
22+
required: true
23+
24+
- type: textarea
25+
id: proposal
26+
attributes:
27+
label: Proposed change
28+
description: How would you like the platform to behave? UI mock-ups, API shape, or just prose are all fine.
29+
validations:
30+
required: true
31+
32+
- type: textarea
33+
id: alternatives
34+
attributes:
35+
label: Alternatives considered
36+
description: Workarounds that exist today, and why they fall short.
37+
validations:
38+
required: false
39+
40+
- type: dropdown
41+
id: surface
42+
attributes:
43+
label: Which surface does this touch?
44+
multiple: true
45+
options:
46+
- API
47+
- Web UI / Dashboard
48+
- Scanner
49+
- Documentation site
50+
- Compliance / Governance / Vendors content
51+
- Deployment / packaging
52+
- Other
53+
validations:
54+
required: true
55+
56+
- type: textarea
57+
id: nis2-context
58+
attributes:
59+
label: NIS2 / regulatory context (optional)
60+
description: |
61+
If the request is driven by a specific Article — Art. 21 sub-paragraph, Art. 18, ACN-specific obligation, GDPR cross-cut, etc. — cite it. It's a tie-breaker on prioritisation.
62+
validations:
63+
required: false

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<!--
2+
Copyright (c) 2026 Fabrizio Salmi <fabrizio.salmi@gmail.com>
3+
SPDX-License-Identifier: AGPL-3.0-only
4+
NIS2 Compliance Platform — https://github.com/fabriziosalmi/nis2-public
5+
6+
Thanks for the contribution. The template below mirrors how the
7+
maintainer writes commit messages and CHANGELOG entries — filling it
8+
in honestly speeds up review and reduces the back-and-forth that
9+
follows a vague PR description.
10+
-->
11+
12+
## Summary
13+
14+
<!-- One or two sentences. What does this change do, and why? -->
15+
16+
## Why
17+
18+
<!--
19+
The motivation. Cite a specific user, audit finding, NIS2 / GDPR
20+
Article, or external-reviewer feedback if applicable. "Cleanup" is
21+
a fine reason; just say so.
22+
-->
23+
24+
## What changes
25+
26+
<!-- A short bulleted list. Files / modules touched, not a diff dump. -->
27+
28+
-
29+
30+
## Surface impact
31+
32+
- [ ] API behaviour change
33+
- [ ] Database schema (migration included)
34+
- [ ] Web UI change (screenshots below)
35+
- [ ] Scanner module
36+
- [ ] Documentation site
37+
- [ ] Deployment / packaging / CI
38+
- [ ] None of the above (refactor / internal / docs only)
39+
40+
## Verification
41+
42+
<!--
43+
How did you test this? Concrete commands beat "tested locally".
44+
If a UI change, attach a screenshot or screen recording.
45+
-->
46+
47+
- [ ] Web build passes (`cd packages/web && npx next build`)
48+
- [ ] Unit tests pass (`make test`)
49+
- [ ] Tested by hand against `make dev`
50+
- [ ] i18n parity preserved (`node` parity check on `messages/*.json`)
51+
- [ ] No new secrets committed (gitleaks-clean)
52+
53+
## NIS2 / GDPR / legal exposure
54+
55+
<!--
56+
If this PR touches anything personal-data-shaped, anything that
57+
changes what the operator (data controller) is told, or anything
58+
that materially shifts the maintainer's exposure under AGPL-3.0
59+
§15-16, flag it here so it can be reflected in CHANGELOG and the
60+
privacy notice as needed.
61+
62+
Most PRs will say "no exposure change" — that's a fine answer.
63+
-->
64+
65+
## Related issues / discussions
66+
67+
Closes #
68+
Refs #

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Thumbs.db
9090
# Misc caches
9191
__pycache__/
9292
.pytest_cache/
93+
.ruff_cache/
9394
*.pyc
9495
# VitePress build artifacts
9596
docs/.vitepress/cache/

.gitleaks.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ regexes = [
4444
'''nis2-doc-locale''',
4545
'''nis2-auth-v\d+''',
4646
'''nis2-dashboard-orientation-v\d+''',
47+
'''nis2-findings-pii-notice-v\d+''',
4748
]

CHANGELOG.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,75 @@
11
# Changelog
22

3+
## [2.5.4] - 2026-04-30
4+
5+
Tier 2 + Tier 3 closure — security hardening on the auth surfaces, GDPR-aware UX on findings, and the OSS-hygiene files (SECURITY.md, security.txt, CODE_OF_CONDUCT, issue/PR templates, CODEOWNERS) the project had been carrying as deferred items.
6+
7+
### 🟠 Security — `/auth/forgot-password` is now constant-time and PII-free in the logs
8+
9+
**Pre-2.5.4 problem.** The unknown-email path early-returned after the `SELECT`, so its wall-clock time was 5–20× shorter than the known-email path (which generates a token, hashes it, INSERTs, and awaits SMTP). A chatty attacker could enumerate registered emails via response timing alone, defeating the always-204 contract. Separately, the MTA-failure log printed `user.email`, promoting a transient operational error into a long-lived PII record and creating a secondary email-enumeration channel for anyone with log access.
10+
11+
**v2.5.4 changes** in `packages/api/app/routers/auth.py::forgot_password`:
12+
13+
1. The unknown-email path now performs the **same** `secrets.token_urlsafe(32)` + SHA-256 hash work as the known-email path and discards the result, so CPU and DB-ish profiles match within a few microseconds.
14+
2. **Both** paths sleep a randomised `asyncio.sleep(uniform(0.05, 0.25))` before returning, so the variable cost of the real `send_email()` call is masked under the same jitter band.
15+
3. The MTA-failure log now records `user.id` only — sufficient for an operator triaging the failure, not an enumeration channel.
16+
17+
The full rationale is in the function docstring so the next reader doesn't strip the defenses thinking they're noise.
18+
19+
### 🟠 Security — CSRF token rotation on `/auth/refresh` (regression-locked)
20+
21+
The double-submit CSRF cookie was already being re-minted on every call to `_build_token_response` — but the invariant was nowhere asserted in tests, and the inline rationale was missing. v2.5.4 adds:
22+
23+
- A focused inline comment in `_build_token_response` explaining *why* the rotation is intentional (a captured CSRF cookie has the same short lifetime as the access token rather than surviving for the full refresh-token window).
24+
- `TestCSRF.test_csrf_token_rotates_on_refresh` in the integration suite, asserting both cookie and response-body reflect a fresh token after `/auth/refresh`.
25+
26+
### 🟠 GDPR — Findings page now carries an explicit personal-data notice
27+
28+
Scan results capture hostnames, IP addresses, employee email addresses (from secrets scanning), and other identifiers that count as personal data under GDPR Art. 4(1). Pre-2.5.4 the operator was given no in-product notice that they are the controller for this evidence and that exports / sharing / retention need to reflect that.
29+
30+
The new dismissible `FindingsPiiNotice` banner sits between the Findings page header and the filters, with content like:
31+
32+
> **I findings possono contenere dati personali**
33+
>
34+
> I risultati delle scansioni possono includere hostname, indirizzi IP, indirizzi email o altri identificatori estratti da fonti pubbliche (DNS, certificate transparency, pattern-matching su secret). Ai sensi del GDPR sei il titolare del trattamento per queste evidenze — applica agli export, alla condivisione e alla retention la stessa cura che applicheresti ai dati personali sottostanti.
35+
36+
Implementation: same SSR-empty-tree pattern as `LegalDisclaimerModal` and `OrientationCard`; versioned `localStorage` key `nis2-findings-pii-notice-v1`; 3 keys × 5 locales (en/it/fr/de/es) = 15 new i18n entries, parity 5/5.
37+
38+
### 🟠 GDPR — `docs/privacy.md` rewritten with concrete data-flow disclosures
39+
40+
`§7 Self-hosted deployments` was a generic "you're the controller" stub. v1.1 splits it into six numbered sub-sections that lay out **specifically** what a self-hosted instance processes:
41+
42+
- **§7.1 Data inventory** — explicit table of every personal-data category, the table it lives in, and the default retention.
43+
- **§7.2 IP address and User-Agent in `audit_logs`** — names the columns, cites the legal basis (Art. 6(1)(f)), documents pseudonymisation on Art. 17 erasure, and points at `AUDIT_LOG_RETENTION_DAYS`.
44+
- **§7.3 Outbound network calls** — names `crt.sh` (Sectigo), DNS resolvers, and target hosts as third-party data flows the deployer has to disclose to data subjects. Reaffirms zero outbound telemetry.
45+
- **§7.4 PII captured incidentally by the scanner** — secrets-scanner output, subdomain enumeration, port banners.
46+
- **§7.5 / §7.6** — what the maintainer is *not* (no Art. 28 contract, no warranty), and the deployer's own GDPR obligations.
47+
48+
Doc version bumped to 1.1; the existing template structure for self-hosted deployers is preserved.
49+
50+
### 🟠 OSS hygiene — SECURITY.md, security.txt, CODE_OF_CONDUCT, issue / PR templates, CODEOWNERS
51+
52+
The project had a thin `SECURITY.md` (3-row supported-versions table, 7-day-fix promise, no PGP, no CRA reference) and no other OSS-hygiene scaffolding. v2.5.4 ships a coordinated rewrite:
53+
54+
- **`SECURITY.md`** — RFC 9116 cross-reference, GitHub-private-disclosure preferred channel, 24h/48h/5d/10d response-by-severity SLAs, **35-day medium-severity patch ceiling** (aligned with EU Cyber Resilience Act Reg. EU 2024/2847 conventions), 90-day coordinated-disclosure default, in-scope / out-of-scope matrix, full enumerated security-measures section reflecting code as it ships in `main`, hall-of-fame placeholder, PGP placeholder until the maintainer's permanent key is provisioned.
55+
- **`packages/web/public/.well-known/security.txt`** — RFC 9116-compliant: GitHub private-disclosure URL + email contact, policy URL, languages `en, it`, canonical URL, `Expires: 2027-04-30`.
56+
- **`CODE_OF_CONDUCT.md`** — verbatim Contributor Covenant 2.1 with the project's community spaces and reporting contact filled in.
57+
- **`.github/ISSUE_TEMPLATE/{bug_report,feature_request}.yml` + `config.yml`** — structured forms with `blank_issues_enabled: false`. The config `contact_links` redirect security reports to the private-advisory flow, questions to Discussions, and dual-license inquiries to email — saving a triage round on miscategorised issues.
58+
- **`.github/PULL_REQUEST_TEMPLATE.md`** — summary / why / surface-impact checkboxes / verification checklist / NIS2-GDPR-legal-exposure prompt. Mirrors the structure of the maintainer's commit-message and CHANGELOG style.
59+
- **`.github/CODEOWNERS`** — every path falls back to `@fabriziosalmi` (single maintainer), with explicit re-listing of the security-sensitive paths (auth, RLS bootstrap, target validator, alembic, infra, gitleaks config) so that growth into multi-maintainer requires no schema change. Documents the *intent* of strictest review on those paths.
60+
61+
### Misc
62+
63+
- `.gitignore`: `.ruff_cache/` added (was leaking into `git status`).
64+
- `.gitleaks.toml`: `nis2-findings-pii-notice-v\d+` added to the localStorage-key allowlist family.
65+
- `API_VERSION` literal bumped 2.5.3 → 2.5.4 in lockstep with `pyproject.toml`.
66+
67+
### Verified
68+
69+
- Web build: green (24/24 pages, /dashboard/findings page builds cleanly).
70+
- 5/5 i18n locales validate; new `findings.piiNotice*` namespace present and aligned (3 keys × 5 locales = 15 entries).
71+
- `.well-known/security.txt` linted by hand against RFC 9116 §2 (Expires set, Contact present, Canonical and Policy URLs absolute).
72+
373
## [2.5.3] - 2026-04-30
474

575
External-reviewer feedback round (Davide, fresh-clone walkthrough). Four concrete fixes — one runtime blocker, one onboarding trap, one operability paper-cut, one orientation gap.

0 commit comments

Comments
 (0)