Compare commits

...

341 Commits

Author SHA1 Message Date
GitHub Actions
20537d7bd9 fix(e2e): add Authorization header to API calls in gaps and webkit specs 2026-03-21 16:21:58 +00:00
Jeremy
66b37b5a98 Merge branch 'development' into fix/cwe-614-secure-cookie-attribute 2026-03-21 12:18:38 -04:00
GitHub Actions
52f759cc00 fix(e2e): pass Authorization header in import session cleanup helpers
- Add getStoredAuthHeader helper that reads charon_auth_token from
  localStorage and constructs an Authorization: Bearer header
- Apply the header to all page.request.* API calls in readImportStatus
  and issuePendingSessionCancel
- The previous code relied on the browser cookie jar for these cleanup
  API calls; with Secure=true on auth cookies, browsers refuse to send
  cookies over HTTP to 127.0.0.1 (IP address, not localhost hostname)
  causing silent 401s that left pending ImportSession rows in the DB
- Unreleased sessions caused all subsequent caddy-import tests to show
  the pending-session banner instead of the Caddyfile textarea, failing
  every test after the first
- The fix mirrors how the React app authenticates: via Authorization
  header, which is transport-independent and works on both HTTP and HTTPS
2026-03-21 14:21:55 +00:00
GitHub Actions
cc3cb1da4b fix(security): harden auth cookie to always set Secure attribute
- Remove the conditional secure=false branch from setSecureCookie that
  allowed cookies to be issued without the Secure flag when requests
  arrived over HTTP from localhost or RFC 1918 private addresses
- Pass the literal true to c.SetCookie directly, eliminating the
  dataflow path that triggered CodeQL go/cookie-secure-not-set (CWE-614)
- Remove the now-dead codeql suppression comment; the root cause is
  gone, not merely silenced
- Update setSecureCookie doc comment to reflect that Secure is always
  true: all major browsers (Chrome 66+, Firefox 75+, Safari 14+) honour
  the Secure attribute on localhost HTTP connections, and direct
  HTTP-on-private-IP access without TLS is an unsupported deployment
  model for Charon which is designed to sit behind Caddy TLS termination
- Update the five TestSetSecureCookie HTTP/local tests that previously
  asserted Secure=false to now assert Secure=true, reflecting the
  elimination of the insecure code path
- Add Secure=true assertion to TestClearSecureCookie to provide explicit
  coverage of the clear-cookie path
2026-03-21 13:17:45 +00:00
GitHub Actions
2c608bf684 docs: track CVE-2026-27171 zlib CPU exhaustion as a known medium vulnerability 2026-03-21 12:30:20 +00:00
Jeremy
a855ed0cf6 Merge pull request #869 from Wikid82/feature/beta-release
fix: resolve security header profile preset slugs when assigning via UUID string
2026-03-21 01:46:32 -04:00
GitHub Actions
ad7e97e7df fix: align test expectations with updated proxy host handler behavior 2026-03-21 03:05:10 +00:00
GitHub Actions
a2fea2b368 fix: update tools list in agent markdown files for consistency 2026-03-21 02:35:28 +00:00
GitHub Actions
c428a5be57 fix: propagate pipeline exit codes in CI quality-checks workflow 2026-03-21 02:23:16 +00:00
GitHub Actions
22769977e3 fix: clarify that advanced_config requires Caddy JSON, not Caddyfile syntax 2026-03-21 02:12:24 +00:00
Jeremy
50fb6659da Merge pull request #863 from Wikid82/feature/beta-release
fix(uptime): fix TCP monitor UX — correct format guidance and add client-side validation
2026-03-20 22:03:08 -04:00
GitHub Actions
e4f2606ea2 fix: resolve security header profile preset slugs when assigning via UUID string 2026-03-21 01:59:34 +00:00
GitHub Actions
af5cdf48cf fix: suppress pgproto3/v2 CVE-2026-4427 alias in vulnerability ignore files 2026-03-21 01:42:18 +00:00
GitHub Actions
1940f7f55d fix(tests): improve DOM order validation for type selector and URL input in CreateMonitorModal 2026-03-21 00:47:03 +00:00
GitHub Actions
c785c5165d fix: validate TCP format and update aria attributes in CreateMonitorModal 2026-03-21 00:47:03 +00:00
GitHub Actions
eaf981f635 fix(deps): update katex to version 0.16.40 and tldts to version 7.0.27 in package-lock.json 2026-03-21 00:47:03 +00:00
GitHub Actions
4284bcf0b6 fix(security): update known vulnerabilities section in SECURITY.md to reflect critical CVE-2025-68121 and additional high-severity issues 2026-03-21 00:47:03 +00:00
GitHub Actions
586f7cfc98 fix(security): enhance vulnerability reporting and documentation in SECURITY.md 2026-03-21 00:47:03 +00:00
GitHub Actions
15e9efeeae fix(security): add security review instructions to Management and QA Security agents 2026-03-21 00:47:03 +00:00
Jeremy
cd8bb2f501 Merge pull request #868 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-20 20:14:19 -04:00
renovate[bot]
fa42e79af3 fix(deps): update non-major-updates 2026-03-21 00:12:20 +00:00
Jeremy
859ddaef1f Merge pull request #867 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-20 14:10:06 -04:00
renovate[bot]
3b247cdd73 fix(deps): update non-major-updates 2026-03-20 18:09:46 +00:00
Jeremy
00aab022f5 Merge pull request #866 from Wikid82/renovate/feature/beta-release-knip-6.x
chore(deps): update dependency knip to v6 (feature/beta-release)
2026-03-20 14:08:29 -04:00
renovate[bot]
a40764d7da chore(deps): update dependency knip to v6 2026-03-20 12:00:39 +00:00
Jeremy
87b3db7019 Merge branch 'development' into feature/beta-release 2026-03-20 02:14:04 -04:00
Jeremy
ded533d690 Merge pull request #865 from Wikid82/renovate/feature/beta-release-nick-fields-retry-4.x
chore(deps): update nick-fields/retry action to v4 (feature/beta-release)
2026-03-20 02:13:46 -04:00
Jeremy
fc4ceafa20 Merge pull request #864 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-20 02:13:31 -04:00
renovate[bot]
5b02eebfe5 chore(deps): update nick-fields/retry action to v4 2026-03-20 05:30:43 +00:00
renovate[bot]
338c9a3eef chore(deps): update non-major-updates 2026-03-20 05:30:39 +00:00
GitHub Actions
68d21fc20b fix: patch CVE-2026-30836 in Caddy build by pinning smallstep/certificates to v0.30.0 2026-03-20 04:15:29 +00:00
GitHub Actions
ea9ebdfdf2 fix(tools): update tools list in agent markdown files for consistency 2026-03-20 04:14:56 +00:00
GitHub Actions
1d09c793f6 fix(uptime): remove 'tcp://' prefix from Redis monitor URL in create and payload validation 2026-03-20 02:57:00 +00:00
GitHub Actions
856fd4097b fix(deps): update undici and tar to latest versions for improved stability 2026-03-20 02:47:00 +00:00
GitHub Actions
bb14ae73cc fix(uptime): fix TCP monitor UX — correct format guidance and add client-side validation
The TCP monitor creation form showed a placeholder that instructed users to enter a URL with the tcp:// scheme prefix (e.g., tcp://192.168.1.1:8080). Following this guidance caused a silent HTTP 500 error because Go's net.SplitHostPort rejects any input containing a scheme prefix, expecting bare host:port format only.

- Corrected the urlPlaceholder translation key to remove the tcp:// prefix
- Added per-type dynamic placeholder (urlPlaceholderHttp / urlPlaceholderTcp) so the URL input shows the correct example format as soon as the user selects a monitor type
- Added per-type helper text below the URL input explaining the required format, updated in real time when the type selector changes
- Added client-side validation: typing a scheme prefix (://) in TCP mode shows an inline error and blocks form submission before the request reaches the backend
- Reordered the Create Monitor form so the type selector appears before the URL input, giving users the correct format context before they type
- Type selector onChange now clears any stale urlError to prevent incorrect error messages persisting after switching from TCP back to HTTP
- Added 5 new i18n keys across all 5 supported locales (en, de, fr, es, zh)
- Added 10 RTL unit tests covering all new validation paths including the type-change error-clear scenario
- Added 9 Playwright E2E tests covering placeholder variants, helper text, inline error lifecycle, submission blocking, and successful TCP creation

Closes #issue-5 (TCP monitor UI cannot add monitor when following placeholder)
2026-03-20 01:19:43 +00:00
Jeremy
44450ff88a Merge pull request #862 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update dependency anchore/grype to v0.110.0 (feature/beta-release)
2026-03-19 19:46:25 -04:00
renovate[bot]
3a80e032f4 chore(deps): update dependency anchore/grype to v0.110.0 2026-03-19 21:09:01 +00:00
Jeremy
6e2d89372f Merge pull request #859 from Wikid82/feature/beta-release
fix(frontend): stabilize CrowdSec first-enable UX and guard empty-value regression
2026-03-19 16:56:50 -04:00
GitHub Actions
5bf7b54496 chore: proactively pin grpc and goxmldsig in Docker builder stages to patch embedded binary CVEs 2026-03-19 18:18:28 +00:00
GitHub Actions
0bdcb2a091 chore: suppress third-party binary CVEs with documented justification and expiry dates 2026-03-19 18:18:28 +00:00
GitHub Actions
b988179685 fix: update @emnapi/core, @emnapi/runtime, baseline-browser-mapping, and i18next to latest versions for improved stability 2026-03-19 18:18:28 +00:00
GitHub Actions
cbfe80809e fix: update @emnapi/core, @emnapi/runtime, and katex to latest versions for improved stability 2026-03-19 18:18:28 +00:00
GitHub Actions
9f826f764c fix: update dependencies in go.work.sum for improved compatibility and performance 2026-03-19 18:18:28 +00:00
Jeremy
262a805317 Merge pull request #861 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-19 14:15:42 -04:00
renovate[bot]
ec25165e54 fix(deps): update non-major-updates 2026-03-19 18:02:03 +00:00
GitHub Actions
7b34e2ecea fix: update google.golang.org/grpc to version 1.79.3 for improved compatibility 2026-03-19 13:10:18 +00:00
GitHub Actions
ec9b8ac925 fix: update @types/debug to version 4.1.13 for improved stability 2026-03-19 12:59:23 +00:00
GitHub Actions
431d88c47c fix: update @tanstack/query-core, @tanstack/react-query, @types/debug, eslint-plugin-testing-library, i18next, and knip to latest versions for improved stability and performance 2026-03-19 12:58:46 +00:00
GitHub Actions
e08e1861d6 fix: update @oxc-project and @rolldown packages to version 1.0.0-rc.10 for improved compatibility 2026-03-19 05:17:14 +00:00
GitHub Actions
64d2d4d423 fix: update ts-api-utils to version 2.5.0 for improved functionality 2026-03-19 05:16:32 +00:00
Jeremy
9f233a0128 Merge pull request #860 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-18 20:30:26 -04:00
renovate[bot]
6939c792bd chore(deps): update non-major-updates 2026-03-18 23:07:56 +00:00
GitHub Actions
853940b74a fix: update mockResolvedValue calls for getSecurityStatus to improve test clarity 2026-03-18 23:06:24 +00:00
GitHub Actions
5aa8940af2 fix: update tools list in agent markdown files for consistency and clarity 2026-03-18 23:04:52 +00:00
GitHub Actions
cd3f2a90b4 fix: seed lapi-status in renderWithSeed to prevent loading gaps 2026-03-18 22:19:22 +00:00
GitHub Actions
bf89c2603d fix: enhance invite token validation for hex format and case sensitivity 2026-03-18 22:15:39 +00:00
GitHub Actions
19b388d865 fix: update Caddy security version to 1.1.50 in Dockerfile 2026-03-18 22:11:50 +00:00
GitHub Actions
25e40f164d fix: replace userEvent.click with user.click for consistency in CrowdSec tests 2026-03-18 22:08:05 +00:00
GitHub Actions
5505f66c41 fix: clarify comments on optimistic updates and server state handling in Security component 2026-03-18 22:06:40 +00:00
GitHub Actions
9a07619b89 fix: assert cloud-metadata error and no raw IPv6 leak for mapped metadata IP 2026-03-18 19:08:55 +00:00
GitHub Actions
faf2041a82 fix: sanitize IPv4-mapped IPv6 address in SSRF error message 2026-03-18 19:06:31 +00:00
GitHub Actions
460834f8f3 fix: use correct checkbox assertion for CrowdSec toggle test 2026-03-18 19:05:16 +00:00
GitHub Actions
75ae77a6bf fix: assert all db.Create calls in uptime service tests 2026-03-18 19:03:53 +00:00
GitHub Actions
73f2134caf fix(tests): improve server readiness check in UptimeService test to prevent misleading failures 2026-03-18 18:45:59 +00:00
GitHub Actions
c5efc30f43 fix: eliminate bcrypt DefaultCost from test setup to prevent CI flakiness 2026-03-18 18:13:18 +00:00
GitHub Actions
3099d74b28 fix: ensure cloud metadata SSRF error is consistent for IPv4-mapped addresses 2026-03-18 17:23:53 +00:00
GitHub Actions
fcc9309f2e chore(deps): update indirect dependencies for improved compatibility and performance 2026-03-18 17:12:01 +00:00
Jeremy
e581a9e7e7 Merge branch 'development' into feature/beta-release 2026-03-18 13:11:50 -04:00
Jeremy
ac72e6c3ac Merge pull request #858 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-18 13:11:20 -04:00
renovate[bot]
db824152ef fix(deps): update non-major-updates 2026-03-18 17:00:26 +00:00
GitHub Actions
1de29fe6fc fix(frontend): stabilize CrowdSec first-enable UX and guard empty-value regression
When CrowdSec is first enabled, the 10-60 second startup window caused
the toggle to immediately flicker back to unchecked, the card badge to
show 'Disabled' throughout startup, CrowdSecKeyWarning to flash before
bouncer registration completed, and CrowdSecConfig to show alarming
LAPI-not-ready banners to the user.

Root cause: the toggle, badge, and warning conditions all read from
stale sources (crowdsecStatus local state and status.crowdsec.enabled
server data) which neither reflects user intent during a pending mutation.

- Derive crowdsecChecked from crowdsecPowerMutation.variables during
  the pending window so the UI reflects intent immediately on click,
  not the lagging server state
- Show a 'Starting...' badge in warning variant throughout the startup
  window so the user knows the operation is in progress
- Suppress CrowdSecKeyWarning unconditionally while the mutation is
  pending, preventing the bouncer key alert from flashing before
  registration completes on the backend
- Broadcast the mutation's running state to the QueryClient cache via
  a synthetic crowdsec-starting key so CrowdSecConfig.tsx can read it
  without prop drilling
- In CrowdSecConfig, suppress the LAPI 'not running' (red) and
  'initializing' (yellow) banners while the startup broadcast is active,
  with a 90-second safety cap to prevent stale state from persisting
  if the tab is closed mid-mutation
- Add security.crowdsec.starting translation key to all five locales
- Add two backend regression tests confirming that empty-string setting
  values are accepted (not rejected by binding validation), preventing
  silent re-introduction of the Issue 4 bug
- Add nine RTL tests covering toggle stabilization, badge text, warning
  suppression, and LAPI banner suppression/expiry
- Add four Playwright E2E tests using route interception to simulate
  the startup delay in a real browser context

Fixes Issues 3 and 4 from the fresh-install bug report.
2026-03-18 16:57:23 +00:00
GitHub Actions
ac2026159e chore: update tailwindcss to version 4.2.2 in package.json 2026-03-18 16:46:50 +00:00
GitHub Actions
cfb28055cf fix: add vulnerability suppressions for CVE-2026-2673 in libcrypto3 and libssl3 with justification and review timeline 2026-03-18 11:08:58 +00:00
GitHub Actions
a2d8970b22 chore: Refactor agent tools for improved organization and efficiency across documentation, frontend development, planning, Playwright testing, QA security, and supervisor roles. 2026-03-18 10:36:14 +00:00
GitHub Actions
abadf9878a chore(deps): update electron-to-chromium to version 1.5.321 2026-03-18 10:27:06 +00:00
GitHub Actions
87590ac4e8 fix: simplify error handling and improve readability in URL validation and uptime service tests 2026-03-18 10:25:25 +00:00
Jeremy
999a81dce7 Merge pull request #857 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update dependency knip to ^5.88.0 (feature/beta-release)
2026-03-18 06:24:40 -04:00
Jeremy
031457406a Merge pull request #855 from Wikid82/feature/beta-release
fix(uptime): allow RFC 1918 IPs for admin-configured monitors
2026-03-18 06:09:51 -04:00
renovate[bot]
3d9d183b77 chore(deps): update dependency knip to ^5.88.0 2026-03-18 10:07:26 +00:00
GitHub Actions
379c664b5c fix(test): align cloud-metadata SSRF handler test with updated error message
The settings handler SSRF test table expected the generic "private ip"
error string for the cloud-metadata case (169.254.169.254). After the
url_validator was updated to return a distinct "cloud metadata" error for
that address, the handler test's errorContains check failed on every CI run.

Updated the test case expectation from "private" to "cloud metadata" to
match the more precise error message now produced by the validator.
2026-03-18 03:38:29 +00:00
GitHub Actions
4d8f09e279 fix: improve readiness checks and error handling in uptime service tests 2026-03-18 03:22:32 +00:00
GitHub Actions
8a0e91ac3b chore: strengthen AllowRFC1918 permit tests to assert success and URL correctness 2026-03-18 03:22:32 +00:00
GitHub Actions
3bc798bc9d fix: normalize IPv4-mapped cloud-metadata address to its IPv4 form before error reporting
- IPv4-mapped cloud metadata (::ffff:169.254.169.254) previously fell through
  the IPv4-mapped IPv6 detection block and returned the generic private-IP error
  instead of the cloud-metadata error, making the two cases inconsistent
- The IPv4-mapped error path used ip.String() (the raw ::ffff:… form) directly
  rather than sanitizeIPForError, potentially leaking the unsanitized IPv6
  address in error messages visible to callers
- Now extracts the IPv4 from the mapped address before both the cloud-metadata
  comparison and the sanitization call, so ::ffff:169.254.169.254 produces the
  same "access to cloud metadata endpoints is blocked" error as 169.254.169.254
  and the error message is always sanitized through the shared helper
- Updated the corresponding test to assert the cloud-metadata message and the
  absence of the raw IPv6 representation in the error text
2026-03-18 03:22:32 +00:00
GitHub Actions
8b4e0afd43 fix: format SeedDefaultSecurityConfig for improved readability 2026-03-18 03:22:32 +00:00
GitHub Actions
c7c4fc8915 fix(deps): update flatted to version 3.4.2 for improved stability 2026-03-18 03:22:32 +00:00
Jeremy
41c0252cf1 Merge pull request #856 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update module github.com/greenpau/caddy-security to v1.1.49 (feature/beta-release)
2026-03-17 23:15:17 -04:00
renovate[bot]
4c375ad86f chore(deps): update module github.com/greenpau/caddy-security to v1.1.49 2026-03-18 02:33:53 +00:00
Jeremy
459a8fef42 Merge branch 'development' into feature/beta-release 2026-03-17 22:32:24 -04:00
GitHub Actions
00a18704e8 fix(uptime): allow RFC 1918 IPs for admin-configured monitors
HTTP/HTTPS uptime monitors targeting LAN addresses (192.168.x.x,
10.x.x.x, 172.16.x.x) permanently reported 'down' on fresh installs
because SSRF protection rejects RFC 1918 ranges at two independent
checkpoints: the URL validator (DNS-resolution layer) and the safe
dialer (TCP-connect layer). Fixing only one layer leaves the monitor
broken in practice.

- Add IsRFC1918() predicate to the network package covering only the
  three RFC 1918 CIDRs; 169.254.x.x (link-local / cloud metadata)
  and loopback are intentionally excluded
- Add WithAllowRFC1918() functional option to both SafeHTTPClient and
  ValidationConfig; option defaults to false so existing behaviour is
  unchanged for every call site except uptime monitors
- In uptime_service.go, pass WithAllowRFC1918() to both
  ValidateExternalURL and NewSafeHTTPClient together; a coordinating
  comment documents that both layers must be relaxed as a unit
- 169.254.169.254 and the full 169.254.0.0/16 link-local range remain
  unconditionally blocked; the cloud-metadata error path is preserved
- 21 new tests across three packages, including an explicit regression
  guard that confirms RFC 1918 blocks are still applied without the
  option set (TestValidateExternalURL_RFC1918BlockedByDefault)

Fixes issues 6 and 7 from the fresh-install bug report.
2026-03-17 21:22:56 +00:00
Jeremy
dc9bbacc27 Merge pull request #854 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update release-drafter/release-drafter digest to 44a942e (feature/beta-release)
2026-03-17 16:41:13 -04:00
Jeremy
4da4e1a0d4 Merge branch 'feature/beta-release' into renovate/feature/beta-release-non-major-updates 2026-03-17 14:37:17 -04:00
Jeremy
3318b4af80 Merge pull request #852 from Wikid82/feature/beta-release
feat(security): seed default SecurityConfig row on application startup
2026-03-17 14:36:45 -04:00
GitHub Actions
c1aaa48ecb chore: cover error path in SeedDefaultSecurityConfig and letsencrypt cert cleanup loop
- The DB error return branch in SeedDefaultSecurityConfig was never
  exercised because all seed tests only ran against a healthy in-memory
  database; added a test that closes the underlying connection before
  calling the function so the FirstOrCreate error path is reached
- The letsencrypt certificate cleanup loop in Register was unreachable
  in all existing tests because no test pre-seeded a ProxyHost with
  an letsencrypt cert association; added a test that creates that
  precondition so the log and Update lines inside the loop execute
- These were the last two files blocking patch coverage on PR #852
2026-03-17 17:45:39 +00:00
renovate[bot]
f82a892405 chore(deps): update release-drafter/release-drafter digest to 44a942e 2026-03-17 17:17:04 +00:00
GitHub Actions
287e85d232 fix(ci): quote shell variables to prevent word splitting in integration test
- All unquoted $i loop counter comparisons and ${TMP_COOKIE} curl
  option arguments in the rate limit integration script were flagged
  by shellcheck SC2086
- Unquoted variables in [ ] test expressions and curl -b/-c options
  can cause subtle failures if the value ever contains whitespace or
  glob characters, and are a shellcheck hard warning that blocks CI
  linting gates
- Quoted all affected variables in place with no logic changes
2026-03-17 17:15:19 +00:00
Jeremy
fa6fbc8ce9 Merge pull request #853 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update paulhatch/semantic-version action to v6.0.2 (feature/beta-release)
2026-03-17 13:14:55 -04:00
GitHub Actions
61418fa9dd fix(security): persist RateLimitMode in Upsert and harden integration test payload
- The security config Upsert update path copied all rate limit fields
  from the incoming request onto the existing database record except
  RateLimitMode, so the seeded default value of "disabled" always
  survived a POST regardless of what the caller sent
- This silently prevented the Caddy rate_limit handler from being
  injected on any container with a pre-existing config record (i.e.,
  every real deployment and every CI run after migration)
- Added the missing field assignment so RateLimitMode is correctly
  persisted on update alongside all other rate limit settings
- Integration test payload now also sends rate_limit_enable alongside
  rate_limit_mode so the handler sync logic fires via its explicit
  first branch, providing belt-and-suspenders correctness independent
  of which path the caller uses to express intent
2026-03-17 17:06:02 +00:00
GitHub Actions
0df1126aa9 fix(deps): update modernc.org/sqlite to version 1.47.0 for improved functionality 2026-03-17 14:31:42 +00:00
renovate[bot]
1c72469ad6 chore(deps): update paulhatch/semantic-version action to v6.0.2 2026-03-17 14:30:44 +00:00
GitHub Actions
338f864f60 fix(ci): set correct rate_limit_mode field in integration test security config
- The rate-limit integration test was sending rate_limit_enable:true in the
  security config POST, but the backend injects the Caddy rate_limit handler
  only when rate_limit_mode is the string "enabled"
- Because rate_limit_mode was absent from the payload, the database default
  of "disabled" persisted and the guard condition always evaluated false,
  leaving the handler uninjected across all 10 verify attempts
- Replaced the boolean rate_limit_enable with the string field
  rate_limit_mode:"enabled" to match the exact contract the backend enforces
2026-03-17 14:29:35 +00:00
GitHub Actions
8b0011f6c6 fix(ci): enhance rate limit integration test reliability
- Added HTTP status checks for login and security config POST requests to ensure proper error handling.
- Implemented a readiness gate for the Caddy admin API before applying security configurations.
- Increased sleep duration before verifying rate limit handler to accommodate Caddy's configuration propagation.
- Changed verification failure from a warning to a hard exit to prevent misleading test results.
- Updated Caddy admin API URL to use the canonical trailing slash in multiple locations.
- Adjusted retry parameters for rate limit verification to reduce polling noise.
- Removed stale GeoIP checksum validation from the Dockerfile's non-CI path to simplify the build process.
2026-03-17 14:05:25 +00:00
GitHub Actions
e6a044c532 fix(deps): update caniuse-lite to version 1.0.30001780 for improved compatibility 2026-03-17 12:40:55 +00:00
GitHub Actions
bb1e59ea93 fix(deps): update bytedance/gopkg to version 0.1.4 for improved functionality 2026-03-17 12:38:43 +00:00
GitHub Actions
b761d7d4f7 feat(security): seed default SecurityConfig row on application startup
On a fresh install the security_configs table is auto-migrated but
contains no rows. Any code path reading SecurityConfig by name received
an empty Go struct with zero values, producing an all-disabled UI state
that offered no guidance to the user and made the security status
endpoint appear broken.

Adds a SeedDefaultSecurityConfig function that uses FirstOrCreate to
guarantee a default row exists with safe, disabled-by-default values on
every startup. The call is idempotent — existing rows are never modified,
so upgrades are unaffected. If the seed fails the application logs a
warning and continues rather than crashing.

Zero-valued rate-limit fields are intentional and safe: the Cerberus
rate-limit middleware applies hardcoded fallback thresholds when the
stored values are zero, so enabling rate limiting without configuring
thresholds results in sensible defaults rather than a divide-by-zero or
traffic block.

Adds three unit tests covering the empty-database, idempotent, and
do-not-overwrite-existing paths.
2026-03-17 12:33:40 +00:00
Jeremy
418fb7d17c Merge pull request #851 from Wikid82/feature/beta-release
fix(settings): allow empty string as a valid setting value
2026-03-16 23:24:37 -04:00
Jeremy
5084483984 Merge branch 'development' into feature/beta-release 2026-03-16 22:05:55 -04:00
GitHub Actions
3c96810aa1 fix(deps): update @babel/helpers, @babel/parser, @babel/runtime, and enhanced-resolve to latest versions for improved stability 2026-03-17 02:05:00 +00:00
GitHub Actions
dcd1ec7e95 fix: improve error handling in TestSettingsHandler_UpdateSetting_EmptyValueAccepted 2026-03-17 02:01:48 +00:00
GitHub Actions
4f222b6308 fix: make 'value' field optional in UpdateSettingRequest struct 2026-03-17 01:40:35 +00:00
Jeremy
071ae38d35 Merge pull request #850 from Wikid82/feature/beta-release
Feature: Pushover Notification Provider
2026-03-16 20:09:08 -04:00
GitHub Actions
3385800f41 fix(deps): update core-js-compat to version 3.49.0 for improved compatibility 2026-03-16 21:48:19 +00:00
GitHub Actions
4fe538b37e chore: add unit tests for Slack and Pushover service flags, and validate Pushover dispatch behavior 2026-03-16 21:38:40 +00:00
Jeremy
2bdf4f8286 Merge branch 'development' into feature/beta-release 2026-03-16 14:26:07 -04:00
Jeremy
a96366957e Merge pull request #849 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-16 14:24:11 -04:00
renovate[bot]
c44642241c chore(deps): update non-major-updates 2026-03-16 18:22:12 +00:00
GitHub Actions
b5bf505ab9 fix: update go-sqlite3 to version 1.14.37 and modernc.org/sqlite to version 1.46.2 for improved stability 2026-03-16 18:20:35 +00:00
GitHub Actions
51f59e5972 fix: update @typescript-eslint packages to version 8.57.1 for improved compatibility and stability 2026-03-16 18:19:36 +00:00
GitHub Actions
65d02e754e feat: add support for Pushover notification provider
- Updated the list of supported notification provider types to include 'pushover'.
- Enhanced the notifications API tests to validate Pushover integration.
- Modified the notifications form to include fields specific to Pushover, such as API Token and User Key.
- Implemented CRUD operations for Pushover providers in the settings.
- Added end-to-end tests for Pushover provider functionality, including form rendering, payload validation, and security checks.
- Updated translations to include Pushover-specific labels and placeholders.
2026-03-16 18:16:14 +00:00
Jeremy
816c0595e1 Merge pull request #834 from Wikid82/feature/beta-release
Feature: Slack Notification Provider
2026-03-16 11:15:29 -04:00
GitHub Actions
9496001811 fix: update undici to version 7.24.4 for improved stability and security 2026-03-16 12:33:58 +00:00
Jeremy
ec1b79c2b7 Merge branch 'development' into feature/beta-release 2026-03-16 08:30:45 -04:00
Jeremy
bab79f2349 Merge pull request #846 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-16 08:28:36 -04:00
renovate[bot]
edd7405313 chore(deps): update non-major-updates 2026-03-16 12:28:25 +00:00
GitHub Actions
79800871fa fix: harden frontend-builder with npm upgrade to mitigate bundled CVEs 2026-03-16 12:26:55 +00:00
Jeremy
67dd87d3a9 Merge pull request #845 from Wikid82/main
Propagate changes from main into development
2026-03-16 08:24:38 -04:00
Jeremy
dfc2beb8f3 Merge pull request #844 from Wikid82/nightly
Weekly: Promote nightly to main (2026-03-16)
2026-03-16 08:16:42 -04:00
GitHub Actions
5e5eae7422 fix: ensure Semgrep hook triggers on Dockerfile-only commits 2026-03-16 11:44:27 +00:00
GitHub Actions
78f216eaef fix: enhance payload handling in Slack provider creation to track token presence 2026-03-16 11:41:06 +00:00
Jeremy
34d5cca972 Merge branch 'main' into nightly 2026-03-16 07:35:56 -04:00
Jeremy
5d771381a1 Merge pull request #842 from Wikid82/bot/update-geolite2-checksum
chore(docker): update GeoLite2-Country.mmdb checksum
2026-03-16 07:35:38 -04:00
GitHub Actions
95a65069c0 fix: handle existing PR outputs in promotion job 2026-03-16 11:17:37 +00:00
Jeremy
1e4b2d1d03 Merge pull request #843 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-16 07:15:40 -04:00
renovate[bot]
81f1dce887 fix(deps): update non-major-updates 2026-03-16 11:06:23 +00:00
Wikid82
3570c05805 chore(docker): update GeoLite2-Country.mmdb checksum
Automated checksum update for GeoLite2-Country.mmdb database.

Old: b79afc28a0a52f89c15e8d92b05c173f314dd4f687719f96cf921012d900fcce
New: aa154fc6bcd712644de232a4abcdd07dac1f801308c0b6f93dbc2b375443da7b

Auto-generated by: .github/workflows/update-geolite2.yml
2026-03-16 02:58:27 +00:00
GitHub Actions
b66cc34e1c fix: update Caddy security version to 1.1.48 in Dockerfile 2026-03-15 20:49:53 +00:00
GitHub Actions
5bafd92edf fix: supply slack webhook token in handler create sub-tests
The slack sub-tests in TestDiscordOnly_CreateRejectsNonDiscord and
TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents were
omitting the required token field from their request payloads.
CreateProvider enforces that Slack providers must have a non-empty
token (the webhook URL) at creation time. Without it the service
returns "slack webhook URL is required", which the handler does not
classify as a 400 validation error, so it falls through to 500.

Add a token field to each test struct, populate it for the slack
case with a valid-format Slack webhook URL, and use
WithSlackURLValidator to bypass the real format check in unit tests —
matching the pattern used in all existing service-level Slack tests.
2026-03-15 15:17:23 +00:00
GitHub Actions
6e4294dce1 fix: validate Slack webhook URL at provider create/update time 2026-03-15 12:23:27 +00:00
GitHub Actions
82b1c85b7c fix: clarify feature flag behavior for Slack notifications in documentation 2026-03-15 12:14:48 +00:00
GitHub Actions
41ecb7122f fix: update baseline-browser-mapping and caniuse-lite to latest versions 2026-03-15 11:58:48 +00:00
GitHub Actions
2fa7608b9b fix: guard routeBodyPromise against indefinite hang in security test 2026-03-15 11:51:16 +00:00
GitHub Actions
285ee2cdda fix: expand Semgrep ruleset to cover TypeScript, Dockerfile, and shell security 2026-03-15 11:45:18 +00:00
GitHub Actions
72598ed2ce fix: inject Slack URL validator via constructor option instead of field mutation 2026-03-15 11:27:51 +00:00
GitHub Actions
8670cdfd2b fix: format notification services table for better readability 2026-03-15 11:17:34 +00:00
GitHub Actions
f8e8440388 fix: correct GeoIP CI detection to require truthy value 2026-03-15 11:15:56 +00:00
GitHub Actions
ab4dee5fcd fix: make Slack webhook URL validator injectable on NotificationService 2026-03-15 11:15:10 +00:00
Jeremy
04e87e87d5 Merge pull request #841 from Wikid82/renovate/feature/beta-release-jsdom-29.x
chore(deps): update dependency jsdom to v29 (feature/beta-release)
2026-03-15 07:00:19 -04:00
Jeremy
cc96435db1 Merge pull request #840 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update softprops/action-gh-release digest to b25b93d (feature/beta-release)
2026-03-15 06:59:51 -04:00
renovate[bot]
53af0a6866 chore(deps): update dependency jsdom to v29 2026-03-15 10:56:03 +00:00
renovate[bot]
3577ce6c56 chore(deps): update softprops/action-gh-release digest to b25b93d 2026-03-15 10:55:54 +00:00
Jeremy
0ce35f2d64 Merge branch 'development' into feature/beta-release 2026-03-14 23:47:43 -04:00
Jeremy
0e556433f7 Merge pull request #839 from Wikid82/hotfix/login
Hotfix: Login / Auth on Private IP
2026-03-14 23:45:41 -04:00
GitHub Actions
4b170b69e0 fix: update Caddy security version to 1.1.47 in Dockerfile 2026-03-15 03:25:41 +00:00
GitHub Actions
fd58f9d99a fix(auth): update SameSite cookie policy description for clarity 2026-03-15 03:23:06 +00:00
GitHub Actions
f33ab83b7c fix(auth): rename isLocalHost to isLocalOrPrivateHost and update related tests 2026-03-15 03:20:11 +00:00
GitHub Actions
6777f6e8ff feat(auth): implement Bearer token fallback in fetchSessionUser for private network HTTP connections
- Expanded fetchSessionUser to include Bearer token from localStorage as a fallback for authentication when Secure cookies fail.
- Updated headers to conditionally include Authorization if a token is present.
- Ensured compatibility with the recent fix for the Secure cookie flag on private network connections.
2026-03-15 02:25:07 +00:00
GitHub Actions
1096b00b94 fix: set PORT environment variable for httpbin backend in integration scripts 2026-03-14 16:44:35 +00:00
GitHub Actions
6180d53a93 fix: update undici to version 7.24.2 in package-lock.json 2026-03-14 16:44:35 +00:00
Jeremy
fca1139c81 Merge pull request #838 from Wikid82/renovate/feature/beta-release-release-drafter-release-drafter-7.x
chore(deps): update release-drafter/release-drafter action to v7 (feature/beta-release)
2026-03-14 12:30:46 -04:00
Jeremy
847b10322a Merge pull request #837 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-14 12:30:29 -04:00
Jeremy
59251c8f27 Merge branch 'feature/beta-release' into renovate/feature/beta-release-non-major-updates 2026-03-14 12:30:02 -04:00
GitHub Actions
58b087bc63 fix: replace curl with wget for backend readiness checks in integration scripts 2026-03-14 13:17:06 +00:00
renovate[bot]
8ab926dc8b chore(deps): update release-drafter/release-drafter action to v7 2026-03-14 13:16:45 +00:00
renovate[bot]
85f258d9f6 chore(deps): update non-major-updates 2026-03-14 13:15:37 +00:00
GitHub Actions
042c5ec6e5 fix(ci): replace abandoned httpbin image with maintained Go alternative 2026-03-13 22:44:19 +00:00
GitHub Actions
05d19c0471 fix: update lru-cache and other dependencies to latest versions 2026-03-13 20:07:30 +00:00
GitHub Actions
48af524313 chore(security): expand Semgrep coverage to include frontend and secrets scanning 2026-03-13 20:07:30 +00:00
GitHub Actions
bad97102e1 fix: repair GeoIP CI detection and harden httpbin startup in integration tests 2026-03-13 20:07:30 +00:00
GitHub Actions
98a4efcd82 fix: handle errors gracefully when commenting on PRs in supply chain verification workflow 2026-03-13 20:07:30 +00:00
Jeremy
f631dfc628 Merge pull request #836 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-13 15:58:41 -04:00
renovate[bot]
eb5b74cbe3 chore(deps): update non-major-updates 2026-03-13 19:08:11 +00:00
GitHub Actions
1785ccc39f fix: remove zlib vulnerability suppression and update review dates for Nebula ECDSA signature malleability 2026-03-13 14:14:22 +00:00
GitHub Actions
4b896c2e3c fix: replace curl with wget for healthcheck commands in Docker configurations 2026-03-13 14:13:37 +00:00
GitHub Actions
88a9cdb0ff fix(deps): update @vitejs/plugin-react to version 6.0.1 and adjust peer dependency for @rolldown/plugin-babel 2026-03-13 12:33:00 +00:00
GitHub Actions
354ff0068a fix: upgrade zlib package in Dockerfile to ensure latest security patches 2026-03-13 12:10:38 +00:00
GitHub Actions
0c419d8f85 chore: add Slack provider validation tests for payload and webhook URL 2026-03-13 12:09:35 +00:00
GitHub Actions
26be592f4d feat: add Slack notification provider support
- Updated the notification provider types to include 'slack'.
- Modified API tests to handle 'slack' as a valid provider type.
- Enhanced frontend forms to display Slack-specific fields (webhook URL and channel name).
- Implemented CRUD operations for Slack providers, ensuring proper payload structure.
- Added E2E tests for Slack notification provider, covering form rendering, validation, and security checks.
- Updated translations to include Slack-related text.
- Ensured that sensitive information (like tokens) is not exposed in API responses.
2026-03-13 03:40:02 +00:00
GitHub Actions
fb9b6cae76 fix(deps): update caddy-security version to 1.1.46 2026-03-13 01:37:09 +00:00
Jeremy
5bb9b2a6fb Merge branch 'development' into feature/beta-release 2026-03-12 13:52:54 -04:00
GitHub Actions
593694a4b4 fix(deps): update goccy/go-json to version 0.10.6 2026-03-12 17:49:05 +00:00
GitHub Actions
b207993299 fix(deps): update baseline-browser-mapping to version 2.10.7 and undici to version 7.23.0 2026-03-12 17:48:14 +00:00
Jeremy
a807288052 Merge pull request #833 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-12 13:45:33 -04:00
renovate[bot]
49b956f916 chore(deps): update non-major-updates 2026-03-12 17:38:44 +00:00
GitHub Actions
53227de55c chore: Refactor code structure for improved readability and maintainability 2026-03-12 10:10:25 +00:00
GitHub Actions
58921556a1 fix(deps): update golang.org/x/term to version 0.41.0 2026-03-12 10:06:34 +00:00
GitHub Actions
442164cc5c fix(deps): update golang.org/x/crypto and golang.org/x/net dependencies to latest versions 2026-03-12 10:05:51 +00:00
Jeremy
8414004d8f Merge pull request #832 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-12 05:53:18 -04:00
renovate[bot]
7932188dae fix(deps): update non-major-updates 2026-03-12 09:30:08 +00:00
GitHub Actions
d4081d954f chore: update dependencies and configuration for Vite and Vitest
- Bump versions of @vitejs/plugin-react, @vitest/coverage-istanbul, @vitest/coverage-v8, and @vitest/ui to their beta releases.
- Upgrade Vite and Vitest to their respective beta versions.
- Adjust Vite configuration to disable code splitting for improved React initialization stability.
2026-03-12 04:31:31 +00:00
GitHub Actions
2e85a341c8 chore: upgrade ESLint and related plugins to version 10.x
- Updated @eslint/js and eslint to version 10.0.0 in package.json.
- Adjusted overrides for eslint-plugin-react-hooks, eslint-plugin-jsx-a11y, and eslint-plugin-promise to ensure compatibility with ESLint v10.
- Modified lefthook.yml to reflect the upgrade and noted the need for plugin support for ESLint v10.
2026-03-12 00:00:01 +00:00
GitHub Actions
2969eb58e4 chore: update TypeScript to 6.0.1-rc and adjust package dependencies
- Removed duplicate @typescript-eslint/utils dependency in frontend/package.json
- Updated TypeScript version from 5.9.3 to 6.0.1-rc in frontend/package.json and package.json
- Adjusted ResizeObserver mock to use globalThis in tests
- Modified tsconfig.json and tsconfig.node.json to include empty types array
- Cleaned up package-lock.json to reflect TypeScript version change and updated dev dependencies
2026-03-11 22:19:35 +00:00
Jeremy
9d6ecd8f73 Merge pull request #824 from Wikid82/feature/beta-release
Feature: Telegram Notification Provider
2026-03-11 14:05:55 -04:00
Jeremy
0c2a9d0ee8 Merge pull request #830 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-11 13:20:47 -04:00
GitHub Actions
c71e6fef30 fix: improve response handling in Telegram notification provider tests to prevent race conditions 2026-03-11 16:54:54 +00:00
renovate[bot]
3186676f94 chore(deps): update non-major-updates 2026-03-11 16:26:55 +00:00
GitHub Actions
b108f11bb4 fix: update zod-validation-error dependency to version 5.0.0 2026-03-11 15:58:43 +00:00
GitHub Actions
d56e8a0f7f fix: update zod dependency version and remove outdated references in package-lock.json 2026-03-11 15:56:33 +00:00
GitHub Actions
b76c1d7efc chore: update golang.org/x/sync dependency to v0.20.0 and remove outdated golang.org/x/text v0.34.0 2026-03-11 15:54:36 +00:00
GitHub Actions
cbb2f42a2b fix: correct syntax error in bulk delete test for ProxyHosts 2026-03-11 15:53:24 +00:00
GitHub Actions
fd056c05a7 feat: Enhance Notifications feature with accessibility improvements and test remediation
- Added aria-label attributes to buttons in Notifications component for better accessibility.
- Updated Notifications tests to use new button interactions and ensure proper functionality.
- Refactored notifications payload tests to mock API responses and validate payload transformations.
- Improved error handling and feedback in notification provider tests.
- Adjusted Telegram notification provider tests to streamline edit interactions.
2026-03-11 15:33:53 +00:00
GitHub Actions
2f76b4eadc fix: update team roster formatting for consistency in Management agent 2026-03-11 15:33:53 +00:00
GitHub Actions
fde59a94ae chore: remove outdated structured autonomy commands and documentation
- Deleted sa-generate.md, sa-implement.md, and sa-plan.md as they are no longer needed.
- Removed security scan commands for CodeQL, Docker image, Go vulnerabilities, GORM, and Trivy due to redundancy.
- Eliminated SQL code review and optimization commands to streamline processes.
- Removed supply chain remediation command as it is now integrated elsewhere.
- Deleted test commands for backend and frontend coverage and unit tests to simplify testing workflow.
- Updated settings.json and CLAUDE.md to reflect the removal of commands and ensure consistency in documentation.
2026-03-11 15:33:53 +00:00
Jeremy
7409862140 Merge pull request #828 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-11 10:57:07 -04:00
renovate[bot]
065ac87815 fix(deps): update non-major-updates 2026-03-11 14:53:49 +00:00
Jeremy
d6d810f1a2 Merge pull request #827 from Wikid82/renovate/feature/beta-release-major-7-react-monorepo
chore(deps): update dependency eslint-plugin-react-hooks to v7 (feature/beta-release)
2026-03-10 22:32:06 -04:00
Jeremy
05c71988c0 Merge pull request #826 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-10 22:31:28 -04:00
GitHub Actions
3e32610ea1 chore: Refactor tests to use findBy queries for better async handling, update mock implementations, and clean up imports across various test files. Adjust toast utility to use for-of loops for callback execution. Update Vite and Vitest configuration files for consistency. 2026-03-11 02:24:28 +00:00
renovate[bot]
be502b7533 chore(deps): update dependency eslint-plugin-react-hooks to v7 2026-03-11 02:15:03 +00:00
renovate[bot]
4e81a982aa chore(deps): update non-major-updates 2026-03-11 02:14:55 +00:00
GitHub Actions
c977c6f9a4 fit(notification): enhance Telegram integration with dynamic API base URL and improved payload validation 2026-03-11 00:34:39 +00:00
GitHub Actions
7416229ba3 fix: restore @types/eslint-plugin-jsx-a11y in devDependencies and remove from dependencies 2026-03-10 23:51:52 +00:00
GitHub Actions
9000c1f4ba chore: add comprehensive tests for Telegram notification service functionality 2026-03-10 23:32:29 +00:00
GitHub Actions
7423e64bc5 fix(dependencies): replace eslint-plugin-vitest with @vitest/eslint-plugin in configuration files 2026-03-10 23:30:08 +00:00
Jeremy
1d5f46980d Merge branch 'development' into feature/beta-release 2026-03-10 14:32:20 -04:00
Jeremy
e09efa42a8 Merge pull request #821 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-10 14:30:09 -04:00
Jeremy
e99be20bae Merge branch 'feature/beta-release' into renovate/feature/beta-release-non-major-updates 2026-03-10 14:29:54 -04:00
GitHub Actions
6ce858e52e fix(dependencies): update ESLint and TypeScript-related packages for compatibility 2026-03-10 18:28:20 +00:00
GitHub Actions
f41bd485e3 fix(docker): update Caddy security version to 1.1.45 2026-03-10 18:20:28 +00:00
GitHub Actions
2fc5b10d3d fix(notifications): surface provider API error details in test failure messages 2026-03-10 17:30:31 +00:00
GitHub Actions
f3d69b0116 feat: add validation to prevent testing new notification providers without saving 2026-03-10 13:23:13 +00:00
renovate[bot]
13c5f8356c chore(deps): update non-major-updates 2026-03-10 13:21:37 +00:00
GitHub Actions
95c3adfa61 fix: update dependencies in package-lock.json for improved compatibility 2026-03-10 12:24:08 +00:00
GitHub Actions
ef71f66029 feat: add Telegram notification provider support
- Updated API to support Telegram as a notification provider type.
- Enhanced tests to cover Telegram provider creation, updates, and token handling.
- Modified frontend forms to include Telegram-specific fields and validation.
- Added localization strings for Telegram provider.
- Implemented security measures to ensure bot tokens are not exposed in API responses.
2026-03-10 12:14:57 +00:00
GitHub Actions
317bff326b fix: update component styles for consistency and improved layout 2026-03-09 20:15:19 +00:00
GitHub Actions
542d4ff3ee fix: replace flex-shrink-0 with shrink-0 for consistent styling across components 2026-03-09 20:03:57 +00:00
GitHub Actions
82a55da026 chore: add @types/eslint-plugin-jsx-a11y as a dependency 2026-03-09 19:46:20 +00:00
GitHub Actions
0535f50d89 fix(deps): update @types/node to version 25.4.0 for improved compatibility 2026-03-09 19:14:11 +00:00
GitHub Actions
fc5cb0eb88 fix(deps): update @types/node to version 25.4.0 for improved compatibility 2026-03-09 19:13:45 +00:00
Jeremy
524d363e27 Merge pull request #820 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-09 15:10:39 -04:00
renovate[bot]
e2ebdb37f0 fix(deps): update non-major-updates 2026-03-09 18:49:35 +00:00
Jeremy
539dd1bff4 Merge pull request #817 from Wikid82/hotfix/docker_build
fix(docker): update CADDY_VERSION to 2.11.2 for improved stability
2026-03-09 14:46:47 -04:00
Jeremy
f8ec567a35 Merge pull request #818 from Wikid82/hotfix/docker_build
fix(docker): update CADDY_VERSION to 2.11.2 for improved stability
2026-03-09 14:46:12 -04:00
Jeremy
c758c9d3ab Merge pull request #813 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-09 13:48:00 -04:00
renovate[bot]
424dc43652 fix(deps): update non-major-updates 2026-03-09 16:47:48 +00:00
GitHub Actions
b0001e4d50 fix: update flatted to version 3.4.1 and i18next to version 25.8.15 2026-03-09 13:12:19 +00:00
GitHub Actions
a77b6c5d3e fix: update tar package to version 7.5.11 2026-03-09 13:11:48 +00:00
GitHub Actions
3414c7c941 fix: update modernc.org/libc to v1.70.0 and golang.org/x/mod to v0.33.0 2026-03-09 13:10:46 +00:00
GitHub Actions
332872c7f5 fix: update Coraza Caddy version to 2.2.0 2026-03-09 12:48:55 +00:00
GitHub Actions
c499c57296 fix: update Caddy security version to 1.1.44 2026-03-09 12:39:22 +00:00
Jeremy
912bb7c577 Merge pull request #800 from Wikid82/feature/beta-release
feat: Enable Email Notifications
2026-03-09 08:36:53 -04:00
GitHub Actions
6a37a906ce fix: update flatted and katex packages to latest versions 2026-03-09 00:52:39 +00:00
GitHub Actions
0f823956c6 fix: add email service check in ShouldUseNotify method 2026-03-09 00:49:07 +00:00
GitHub Actions
703108051a fix: correct spelling of "Commit" in agent documentation 2026-03-09 00:45:50 +00:00
GitHub Actions
795486e5b2 fix: correct typo in Multi-Commit Slicing Protocol section 2026-03-09 00:44:10 +00:00
GitHub Actions
799ca8c5f9 fix: enhance decompression limit check to prevent false positives for valid files 2026-03-09 00:42:23 +00:00
GitHub Actions
9cc7393e7b fix: update digest references in nightly build workflow to use output from resolve_digest step 2026-03-09 00:28:55 +00:00
GitHub Actions
791e812c3c fix: add assertion for ExpiresAt field in ManualChallenge struct 2026-03-09 00:09:14 +00:00
GitHub Actions
187c3aea68 fix: remove unused tags output from build-and-push-nightly job 2026-03-09 00:06:00 +00:00
GitHub Actions
d7de28a040 fix: allow saving email notification providers and render HTML body correctly 2026-03-08 20:26:13 +00:00
GitHub Actions
d1baf6f1b0 feat: implement email provider testing functionality and corresponding unit tests 2026-03-08 16:14:08 +00:00
GitHub Actions
3201830405 chore: update dependencies for golang.org/x/time, golang.org/x/arch, and golang.org/x/sys 2026-03-08 15:52:44 +00:00
GitHub Actions
728a55f1d8 fix: simplify frontend lint command in lefthook configuration 2026-03-08 08:06:50 +00:00
GitHub Actions
d3ef8d83b3 fix(frontend): resolve ESLint crash and repair lint configuration
- Scope base JS/TS configs to only JS/TS file extensions, preventing
  TypeError when ESLint applies core rules to markdown/CSS/JSON files
- Remove silent data loss from duplicate JSON keys in five translation
  files where the second dashboard block was overriding the first
- Fix unsafe optional chaining in CredentialManager that would throw
  TypeError when providerTypeInfo is undefined
- Remove stale eslint-disable directive for a rule now handled globally
  by the unused-imports plugin
- Downgrade high-volume lint rules (testing-library, jsx-a11y, import-x,
  vitest) from error to warn to unblock development while preserving
  visibility for incremental cleanup
2026-03-08 07:45:01 +00:00
GitHub Actions
c4e8d6c8ae chore: add unit tests for certificate handler, logs websocket upgrader, config loading, and mail service 2026-03-08 05:45:21 +00:00
GitHub Actions
698ad86d17 chore: structured autonomy commands for planning, generating, and implementing features
- Create sa-generate.md for generating implementation documentation from plans
- Create sa-implement.md for executing implementation plans step-by-step
- Create sa-plan.md for collaborating with users to design development plans
- Add security scan commands for CodeQL, Docker images, Go vulnerabilities, and GORM
- Implement SQL code review and optimization commands
- Add supply chain vulnerability remediation process
- Introduce backend and frontend test commands with coverage checks
- Update settings.json for command permissions
- Document governance, project overview, code quality rules, and critical architecture rules in CLAUDE.md
- Establish root cause analysis protocol and definition of done for development
2026-03-08 05:45:21 +00:00
Jeremy
2240c4c629 Merge pull request #812 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update release-drafter/release-drafter digest to 6a93d82 (feature/beta-release)
2026-03-07 18:01:09 -05:00
GitHub Actions
65b82a8e08 feat: add email notification provider with HTML templates
- Implemented email notification functionality in the NotificationService.
- Added support for rendering email templates based on event types.
- Created HTML templates for various notification types (security alerts, SSL events, uptime events, and system events).
- Updated the dispatchEmail method to utilize the new email templates.
- Added tests for email template rendering and fallback mechanisms.
- Enhanced documentation to include email notification setup and usage instructions.
- Introduced end-to-end tests for the email notification provider in the settings.
2026-03-07 19:54:21 +00:00
renovate[bot]
8032fb5b41 chore(deps): update non-major-updates 2026-03-07 19:54:06 +00:00
Jeremy
56fde3cbe1 Merge pull request #811 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update dependency knip to ^5.86.0 (feature/beta-release)
2026-03-07 14:53:33 -05:00
renovate[bot]
bccbb708f1 chore(deps): update dependency knip to ^5.86.0 2026-03-07 17:27:31 +00:00
GitHub Actions
80b1ed7fab fix: update knip to version 5.86.0 and upgrade oxc-resolver to 11.19.1; add unbash and yaml packages 2026-03-07 13:59:37 +00:00
GitHub Actions
e68035fe30 fix: add Trivy ignore for CVE-2026-22184 and update expiry date for CVE-2026-22184 in Grype configuration 2026-03-07 13:56:01 +00:00
GitHub Actions
80ecb7de7f fix: enhance vulnerability reporting in nightly build with detailed triage information 2026-03-07 13:38:16 +00:00
GitHub Actions
75cd0a4d9c fix: update nightly branch checkout reference to support manual triggers 2026-03-07 12:58:40 +00:00
GitHub Actions
2824a731f5 fix: improve Alpine image digest resolution in nightly build workflow 2026-03-07 12:40:00 +00:00
GitHub Actions
2dbb00036d fix: resolve image digest from GHCR API for nightly builds 2026-03-07 12:25:57 +00:00
GitHub Actions
0ad0c2f2c4 fix: improve error handling for empty build digest in Syft SBOM scan 2026-03-07 12:18:20 +00:00
GitHub Actions
104f0eb6ee fix: add error handling for empty build digest in Syft SBOM scan 2026-03-07 12:04:15 +00:00
GitHub Actions
c144bb2b97 fix: enhance email notification formatting with HTML for improved readability 2026-03-07 05:53:46 +00:00
Jeremy
f50b05519b Merge pull request #810 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update aquasecurity/trivy-action action to v0.35.0 (feature/beta-release)
2026-03-07 00:35:57 -05:00
GitHub Actions
ca3c1085ac fix: update notification messages for various handlers to improve clarity and consistency 2026-03-07 05:16:07 +00:00
renovate[bot]
4cee4f01f3 chore(deps): update aquasecurity/trivy-action action to v0.35.0 2026-03-07 04:29:40 +00:00
GitHub Actions
82e2134333 fix: remove security-experimental queries from CodeQL configuration to prevent false positives 2026-03-07 03:48:04 +00:00
GitHub Actions
6add11f1d2 fix: update pre-commit hooks to process all files instead of staged files for end-of-file and trailing whitespace checks 2026-03-07 03:44:18 +00:00
GitHub Actions
744b6aeff5 fix: improve pagination handling and prevent decompression bombs in backup service
fix: enhance JWT secret management to avoid hardcoded values and ensure security
feat: add SMTP address sanitization to prevent email header injection vulnerabilities
2026-03-07 03:39:54 +00:00
GitHub Actions
92310a8b3e fix: update CodeQL queries to include security-experimental suite for enhanced analysis 2026-03-07 02:42:42 +00:00
GitHub Actions
d74ea47e2c fix: enhance pre-commit hooks to auto-fix end-of-file and trailing whitespace issues, and re-stage modified files for review 2026-03-07 02:26:30 +00:00
GitHub Actions
c665f62700 chore: migrate pre-commit hooks to lefthook for improved performance and consistency 2026-03-07 02:20:29 +00:00
GitHub Actions
37471141e8 fix: update eslint and related dependencies to latest versions for improved functionality 2026-03-07 02:07:31 +00:00
GitHub Actions
81497beb4b fix: update opentelemetry dependencies to latest versions for improved performance 2026-03-07 02:06:15 +00:00
GitHub Actions
2d40f34ff0 chore: add lefthook configuration for pre-commit and pre-push pipelines 2026-03-07 02:02:37 +00:00
Jeremy
801760add1 Potential fix for code scanning alert no. 1271: Email content injection
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-06 15:30:55 -05:00
GitHub Actions
4ebf8d23fe fix: enhance email sanitization by trimming whitespace and normalizing input 2026-03-06 20:18:51 +00:00
GitHub Actions
77a7368c5d fix: update caddy-security version to 1.1.43 for improved security 2026-03-06 20:18:36 +00:00
Jeremy
51a01c4f7b Merge pull request #809 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-06 15:01:01 -05:00
renovate[bot]
13d31dd922 fix(deps): update non-major-updates 2026-03-06 20:00:48 +00:00
GitHub Actions
c9bb303a7d fix: update dependencies for eslint, caniuse-lite, react-i18next, tldts, and tldts-core to latest versions for improved functionality 2026-03-06 19:59:45 +00:00
GitHub Actions
6ebfd417e3 fix: update katex and tldts dependencies to latest versions for improved functionality 2026-03-06 19:58:58 +00:00
GitHub Actions
b527470e75 fix: update opentelemetry dependencies to v1.42.0 for improved functionality and performance 2026-03-06 19:58:19 +00:00
GitHub Actions
89b4d88eb1 fix: enhance email content sanitization to prevent CR/LF injection and improve security 2026-03-06 19:56:22 +00:00
GitHub Actions
a69f698440 fix: enhance WebSocket origin check and improve email validation in mail service 2026-03-06 13:50:59 +00:00
GitHub Actions
ee224adcf1 fix: update notification provider type in tests and enhance email injection sanitization 2026-03-06 06:31:11 +00:00
GitHub Actions
5bbae48b6b chore(docker): wire all workflows to single-source version ARGs
The Dockerfile already centralizes all version pins into top-level ARGs
(GO_VERSION, ALPINE_IMAGE, CROWDSEC_VERSION, EXPR_LANG_VERSION, XNET_VERSION).
This change closes the remaining gaps so those ARGs are the single source of
truth end-to-end:

- nightly-build.yml now resolves the Alpine image digest at build time and
  passes ALPINE_IMAGE as a build-arg, matching the docker-build.yml pattern.
  Previously, nightly images were built with the Dockerfile ARG default and
  without a pinned digest, making runtime Alpine differ from docker-build.yml.

- six CI workflows (quality-checks, codecov-upload, benchmark, e2e-tests-split,
  release-goreleaser, codeql) declared a GO_VERSION env var but their setup-go
  steps ignored it and hardcoded the version string directly. They now reference
  ${{ env.GO_VERSION }}, so Renovate only needs to update one value per file
  and the env var actually serves its purpose.

- codeql.yml had no GO_VERSION env var at all; one is now added alongside the
  existing GOTOOLCHAIN: auto entry.

When Renovate bumps Go, it updates the env var at the top of each workflow and
the Dockerfile ARG — zero manual hunting required.
2026-03-06 03:57:18 +00:00
GitHub Actions
abcfd62b21 fix: update Go version to 1.26.1 in CodeQL workflow for consistency and security improvements 2026-03-06 03:20:37 +00:00
GitHub Actions
10d952a22e fix: update golang version to 1.26.1-alpine in Dockerfile for security improvements 2026-03-06 03:14:16 +00:00
GitHub Actions
635caf0f9a fix: update Caddy version to 2.11.2 in architecture and compatibility matrix for consistency 2026-03-06 02:56:31 +00:00
GitHub Actions
2266a8d051 fix: update golang version to 1.26.1-alpine in Dockerfile for consistency and security improvements 2026-03-06 02:44:07 +00:00
GitHub Actions
b292a1b793 fix: update Go version to 1.26.1 in multiple workflow files for consistency and security improvements 2026-03-06 02:35:36 +00:00
GitHub Actions
bf398a1cb2 fix: update Go version to 1.26.1 in Dockerfile and go.work for security improvements 2026-03-06 02:22:38 +00:00
GitHub Actions
e7c98e5526 fix: update golang version to 1.26.1-alpine in Dockerfile for security improvements 2026-03-06 02:15:37 +00:00
Jeremy
99ff0a34e3 Merge pull request #808 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update non-major-updates (feature/beta-release)
2026-03-05 21:07:44 -05:00
GitHub Actions
c42b7f5a5b fix: update golang.org/x/net to version 0.51.0 in Dockerfile 2026-03-06 02:06:49 +00:00
GitHub Actions
ed89295012 feat: wire MailService into notification dispatch pipeline (Stage 3)
Unifies the two previously independent email subsystems — MailService
(net/smtp transport) and NotificationService (HTTP-based providers) —
so email can participate in the notification dispatch pipeline.

Key changes:
- SendEmail signature updated to accept context.Context and []string
  recipients to enable timeout propagation and multi-recipient dispatch
- NotificationService.dispatchEmail() wires MailService as a first-class
  provider type with IsConfigured() guard and 30s context timeout
- 'email' added to isSupportedNotificationProviderType() and
  supportsJSONTemplates() returns false for email (plain/HTML only)
- settings_handler.go test-email endpoint updated to new SendEmail API
- Frontend: 'email' added to provider type union in notifications.ts,
  Notifications.tsx shows recipient field and hides URL/token fields for
  email providers
- All existing tests updated to match new SendEmail signature
- New tests added covering dispatchEmail paths, IsConfigured guards,
  recipient validation, and context timeout behaviour

Also fixes confirmed false-positive CodeQL go/email-injection alerts:
- smtp.SendMail, sendSSL w.Write, and sendSTARTTLS w.Write sites now
  carry inline codeql[go/email-injection] annotations as required by the
  CodeQL same-line suppression spec; preceding-line annotations silently
  no-op in current CodeQL versions
- auth_handler.go c.SetCookie annotated for intentional Secure=false on
  local non-HTTPS loopback (go/cookie-secure-not-set warning only)

Closes part of #800
2026-03-06 02:06:49 +00:00
renovate[bot]
834907cb5d chore(deps): update non-major-updates 2026-03-06 02:02:10 +00:00
Jeremy
e295a1f64c Merge pull request #806 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update dependency @types/node to ^25.3.4 (feature/beta-release)
2026-03-05 20:58:50 -05:00
Jeremy
7cec4d7979 Merge pull request #807 from Wikid82/renovate/feature/beta-release-docker-build-push-action-7.x
chore(deps): update docker/build-push-action action to v7 (feature/beta-release)
2026-03-05 20:58:10 -05:00
renovate[bot]
132bbbd657 chore(deps): update docker/build-push-action action to v7 2026-03-06 01:07:01 +00:00
renovate[bot]
833220f1cb chore(deps): update dependency @types/node to ^25.3.4 2026-03-06 01:06:56 +00:00
Jeremy
e1e422bfc6 Merge pull request #805 from Wikid82/renovate/feature/beta-release-docker-metadata-action-6.x
chore(deps): update docker/metadata-action action to v6 (feature/beta-release)
2026-03-05 20:02:26 -05:00
Jeremy
e4b6ce62cd Merge pull request #804 from Wikid82/renovate/feature/beta-release-non-major-updates
fix(deps): update non-major-updates (feature/beta-release)
2026-03-05 20:01:13 -05:00
renovate[bot]
396d01595e chore(deps): update docker/metadata-action action to v6 2026-03-05 21:12:58 +00:00
renovate[bot]
6a13e648ea fix(deps): update non-major-updates 2026-03-05 21:12:51 +00:00
GitHub Actions
5fa0cff274 fix: eliminate wall-clock race in TestApplyRepullsOnCacheExpired
The test used a 5ms TTL with a 10ms wall-clock sleep to simulate cache
expiry. On loaded CI runners (Azure eastus), the repull HTTP round-trip
plus disk I/O for Store easily exceeded 5ms, causing the freshly written
cache entry to also appear expired when Load was called immediately after,
producing a spurious 'cache expired' error.

HubCache already exposes a nowFn field for deterministic time injection.
Replace the sleep-based approach with a nowFn that advances the clock 2
hours, making the initial entry appear expired to Apply while keeping the
freshly re-stored entry (retrieved_at ≈ now+2h, TTL=1h) valid for the
final assertion.
2026-03-05 20:20:14 +00:00
GitHub Actions
bcb2748f89 fix: update CADDY_SECURITY_VERSION to 1.1.42 in Dockerfile 2026-03-05 20:09:13 +00:00
GitHub Actions
e68a6039b9 fix: update css-syntax-patches-for-csstree to version 1.1.0 and react-i18next to version 16.5.5 in package-lock.json 2026-03-05 20:04:48 +00:00
GitHub Actions
0199f93994 fix: update katex version to 0.16.35 in package-lock.json 2026-03-05 20:04:30 +00:00
GitHub Actions
f2cf5c3508 chore: add coverage for default false state of email notifications feature flag 2026-03-05 14:58:21 +00:00
GitHub Actions
1d39756713 fix: update css-tree version to 3.2.1 in package-lock.json 2026-03-05 14:56:25 +00:00
GitHub Actions
71455ef88f fix: update katex version to 0.16.34 in package-lock.json 2026-03-05 14:56:16 +00:00
Jeremy
99b8ed875e Merge pull request #803 from Wikid82/renovate/feature/beta-release-docker-setup-buildx-action-4.x
chore(deps): update docker/setup-buildx-action action to v4 (feature/beta-release)
2026-03-05 09:41:29 -05:00
Jeremy
8242666678 Merge pull request #802 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update github/codeql-action digest to 0c0c5dc (feature/beta-release)
2026-03-05 09:40:59 -05:00
renovate[bot]
5aade0456e chore(deps): update docker/setup-buildx-action action to v4 2026-03-05 14:39:50 +00:00
renovate[bot]
479f56f3e8 chore(deps): update github/codeql-action digest to 0c0c5dc 2026-03-05 14:39:43 +00:00
GitHub Actions
8c7a55eaa2 fix: pin Trivy binary version to v0.69.3 in all CI workflows 2026-03-05 13:04:33 +00:00
GitHub Actions
924b8227b5 fix: add bash to Dockerfile dependencies for xcaddy build process 2026-03-05 07:15:37 +00:00
Jeremy
c3fa29d13c Merge branch 'development' into feature/beta-release 2026-03-05 02:13:58 -05:00
Jeremy
e5dab58b42 Merge pull request #801 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update dependency tar to ^7.5.10 (feature/beta-release)
2026-03-05 02:13:27 -05:00
Jeremy
22496a44a8 Merge branch 'feature/beta-release' into renovate/feature/beta-release-non-major-updates 2026-03-05 02:07:52 -05:00
GitHub Actions
87e6762611 fix: pin alpine and golang images with specific SHA256 digests in Dockerfile 2026-03-05 07:05:04 +00:00
GitHub Actions
ddc79865bc test: cover email provider paths in SendExternal and TestProvider
Two unit tests cover the code paths introduced when email was registered
as a recognised notification provider type in Stage 2.

- TestSendExternal_EmailProviderSkipsJSONTemplate exercises the goroutine
  warn path where an enabled email provider passes isDispatchEnabled but
  fails supportsJSONTemplates, producing a warning log without panicking
- TestTestProvider_EmailRejectsJSONTemplateStep asserts TestProvider
  returns a clear error for email providers because the JSON template
  dispatch path does not apply to email delivery

Patch coverage: 6/6 changed lines covered (100%)
2026-03-05 06:57:37 +00:00
renovate[bot]
6ee185c538 chore(deps): update dependency tar to ^7.5.10 2026-03-05 06:39:58 +00:00
GitHub Actions
367943b543 fix: update caddy-security version to 1.1.38 in Dockerfile 2026-03-05 06:36:59 +00:00
GitHub Actions
08e7eb7525 fix: update css-tree and mdn-data package versions to latest 2026-03-05 04:44:10 +00:00
GitHub Actions
35ca99866a fix: update tar package version from 7.5.9 to 7.5.10 2026-03-05 04:43:10 +00:00
GitHub Actions
2f83526966 fix: resolve email provider test regression from Stage 2 flag registration
After email was recognised as a supported provider type, the existing
rejection assertion for unsupported types incorrectly included email
in its denial list, causing a nil-dereference panic.

- Remove email from the unsupported-type rejection list and cover it
  in the accepted-types path instead
- Correct allFeaturesEnabled fixture to set email flag to true, keeping
  the fixture semantically consistent with all other service flags
2026-03-05 04:22:04 +00:00
GitHub Actions
5a58404e1b feat: register email as feature-flagged notification service
Add email as a recognized, feature-flagged notification service type.
The flag defaults to false and acts as a dispatch gate alongside the
existing discord, gotify, and webhook notification service flags.

- Add FlagEmailServiceEnabled constant to the notifications feature flag
  registry with the canonical key convention
- Register the flag in the handler defaults so it appears in the feature
  flags API response with a false default
- Recognise 'email' as a supported notification provider type so that
  providers of this type pass the type validation gate
- Gate email dispatch on the new flag in isDispatchEnabled() following
  the same pattern as gotify and webhook service flags
- Expand the E2E test fixtures FeatureFlags interface to include the new
  flag key so typed fixture objects remain accurate

No email message dispatch is wired in this commit; the flag registration
alone makes the email provider type valid and toggleable.
2026-03-05 03:36:27 +00:00
GitHub Actions
8ea907066b chore: remove Shoutrrr residue and dead notification legacy code
Remove all deprecated Shoutrrr integration artifacts and dead legacy fallback
code from the notification subsystem.

- Remove legacySendFunc field, ErrLegacyFallbackDisabled error, and
  legacyFallbackInvocationError() from notification service
- Delete ShouldUseLegacyFallback() from notification router; simplify
  ShouldUseNotify() by removing now-dead providerEngine parameter
- Remove EngineLegacy engine constant; EngineNotifyV1 is the sole engine
- Remove legacy.fallback_enabled feature flag, retiredLegacyFallbackEnvAliases,
  and parseFlagBool/resolveRetiredLegacyFallback helpers from flags handler
- Remove orphaned EmailRecipients field from NotificationConfig model
- Delete feature_flags_coverage_v2_test.go (tested only the retired flag path)
- Delete security_notifications_test.go.archived (stale archived file)
- Move FIREFOX_E2E_FIXES_SUMMARY.md to docs/implementation/
- Remove root-level scan artifacts tracked in error; add gitignore patterns to
  prevent future tracking of trivy-report.json and related outputs
- Update ARCHITECTURE.instructions.md: Notifications row Shoutrrr → Notify

No functional changes to active notification dispatch or mail delivery.
2026-03-05 00:41:42 +00:00
GitHub Actions
ffe5d951e0 fix: update terminology from "PR Slicing Strategy" to "Cmmit Slicing Strategy" in agent instructions 2026-03-04 21:02:59 +00:00
Jeremy
e5af7d98d1 Merge pull request #799 from Wikid82/renovate/feature/beta-release-non-major-updates
chore(deps): update github/codeql-action digest to b6dfacb (feature/beta-release)
2026-03-04 13:38:58 -05:00
GitHub Actions
27c252600a chore: git cache cleanup 2026-03-04 18:34:49 +00:00
GitHub Actions
c32cce2a88 chore: git cache cleanup 2026-03-04 18:34:39 +00:00
renovate[bot]
c01c6c6225 chore(deps): update github/codeql-action digest to b6dfacb 2026-03-04 18:33:32 +00:00
519 changed files with 27712 additions and 8107 deletions

View File

@@ -47,7 +47,7 @@ services:
# - <PATH_TO_YOUR_CADDYFILE>:/import/Caddyfile:ro
# - <PATH_TO_YOUR_SITES_DIR>:/import/sites:ro # If your Caddyfile imports other files
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/v1/health || exit 1"]
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:8080/api/v1/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -87,7 +87,7 @@ services:
- playwright_caddy_config:/config
- /var/run/docker.sock:/var/run/docker.sock:ro # For container discovery in tests
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/api/v1/health"]
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:8080/api/v1/health || exit 1"]
interval: 5s
timeout: 3s
retries: 12

View File

@@ -52,7 +52,7 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro # For container discovery in tests
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/v1/health || exit 1"]
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:8080/api/v1/health || exit 1"]
interval: 5s
timeout: 5s
retries: 10

View File

@@ -52,7 +52,7 @@ services:
# - ./my-existing-Caddyfile:/import/Caddyfile:ro
# - ./sites:/import/sites:ro # If your Caddyfile imports other files
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/v1/health || exit 1"]
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:8080/api/v1/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3

View File

@@ -365,7 +365,7 @@ echo "Caddy started (PID: $CADDY_PID)"
echo "Waiting for Caddy admin API..."
i=1
while [ "$i" -le 30 ]; do
if curl -sf http://127.0.0.1:2019/config/ > /dev/null 2>&1; then
if wget -qO /dev/null http://127.0.0.1:2019/config/ 2>/dev/null; then
echo "Caddy is ready!"
break
fi

View File

@@ -9,13 +9,12 @@
.git/
.gitignore
.github/
.pre-commit-config.yaml
codecov.yml
.goreleaser.yaml
.sourcery.yml
# -----------------------------------------------------------------------------
# Python (pre-commit, tooling)
# Python (tooling)
# -----------------------------------------------------------------------------
__pycache__/
*.py[cod]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -24,12 +24,12 @@ You are "lazy" in the smartest way possible. You never do what a subordinate can
4. **Team Roster**:
- `Planning`: The Architect. (Delegate research & planning here).
- `Supervisor`: The Senior Advisor. (Delegate plan review here).
- `Backend_Dev`: The Engineer. (Delegate Go implementation here).
- `Frontend_Dev`: The Designer. (Delegate React implementation here).
- `QA_Security`: The Auditor. (Delegate verification and testing here).
- `Docs_Writer`: The Scribe. (Delegate docs here).
- `Backend Dev`: The Engineer. (Delegate Go implementation here).
- `Frontend Dev`: The Designer. (Delegate React implementation here).
- `QA Security`: The Auditor. (Delegate verification and testing here).
- `Docs Writer`: The Scribe. (Delegate docs here).
- `DevOps`: The Packager. (Delegate CI/CD and infrastructure here).
- `Playwright_Dev`: The E2E Specialist. (Delegate Playwright test creation and maintenance here).
- `Playwright Dev`: The E2E Specialist. (Delegate Playwright test creation and maintenance here).
5. **Parallel Execution**:
- You may delegate to `runSubagent` multiple times in parallel if tasks are independent. The only exception is `QA_Security`, which must run last as this validates the entire codebase after all changes.
6. **Implementation Choices**:
@@ -43,7 +43,7 @@ You are "lazy" in the smartest way possible. You never do what a subordinate can
- **Identify Goal**: Understand the user's request.
- **STOP**: Do not look at the code. Do not run `list_dir`. No code is to be changed or implemented until there is a fundamentally sound plan of action that has been approved by the user.
- **Action**: Immediately call `Planning` subagent.
- *Prompt*: "Research the necessary files for '{user_request}' and write a comprehensive plan detailing as many specifics as possible to `docs/plans/current_spec.md`. Be an artist with directions and discriptions. Include file names, function names, and component names wherever possible. Break the plan into phases based on the least amount of requests. Include a PR Slicing Strategy section that decides whether to split work into multiple PRs and, when split, defines PR-1/PR-2/PR-3 scope, dependencies, and acceptance criteria. Review and suggest updaetes to `.gitignore`, `codecov.yml`, `.dockerignore`, and `Dockerfile` if necessary. Return only when the plan is complete."
- *Prompt*: "Research the necessary files for '{user_request}' and write a comprehensive plan detailing as many specifics as possible to `docs/plans/current_spec.md`. Be an artist with directions and discriptions. Include file names, function names, and component names wherever possible. Break the plan into phases based on the least amount of requests. Include a Commit Slicing Strategy section that decides whether to split work into multiple PRs and, when split, defines PR-1/PR-2/PR-3 scope, dependencies, and acceptance criteria. Review and suggest updaetes to `.gitignore`, `codecov.yml`, `.dockerignore`, and `Dockerfile` if necessary. Return only when the plan is complete."
- **Task Specifics**:
- If the task is to just run tests or audits, there is no need for a plan. Directly call `QA_Security` to perform the tests and write the report. If issues are found, return to `Planning` for a remediation plan and delegate the fixes to the corresponding subagents.
@@ -59,7 +59,7 @@ You are "lazy" in the smartest way possible. You never do what a subordinate can
- **Ask**: "Plan created. Shall I authorize the construction?"
4. **Phase 4: Execution (Waterfall)**:
- **Single-PR or Multi-PR Decision**: Read the PR Slicing Strategy in `docs/plans/current_spec.md`.
- **Single-PR or Multi-PR Decision**: Read the Commit Slicing Strategy in `docs/plans/current_spec.md`.
- **If single PR**:
- **Backend**: Call `Backend_Dev` with the plan file.
- **Frontend**: Call `Frontend_Dev` with the plan file.
@@ -73,7 +73,8 @@ You are "lazy" in the smartest way possible. You never do what a subordinate can
- **Supervisor**: Call `Supervisor` to review the implementation against the plan. Provide feedback and ensure alignment with best practices.
6. **Phase 6: Audit**:
- **QA**: Call `QA_Security` to meticulously test current implementation as well as regression test. Run all linting, security tasks, and manual pre-commit checks. Write a report to `docs/reports/qa_report.md`. Start back at Phase 1 if issues are found.
- Review Security: Read `security.md.instrutctions.md` and `SECURITY.md` to understand the security requirements and best practices for Charon. Ensure that any open concerns or issues are addressed in the QA Audit and `SECURITY.md` is updated accordingly.
- **QA**: Call `QA_Security` to meticulously test current implementation as well as regression test. Run all linting, security tasks, and manual lefthook checks. Write a report to `docs/reports/qa_report.md`. Start back at Phase 1 if issues are found.
7. **Phase 7: Closure**:
- **Docs**: Call `Docs_Writer`.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -130,7 +130,7 @@ graph TB
| **WebSocket** | gorilla/websocket | Latest | Real-time log streaming |
| **Crypto** | golang.org/x/crypto | Latest | Password hashing, encryption |
| **Metrics** | Prometheus Client | Latest | Application metrics |
| **Notifications** | Shoutrrr | Latest | Multi-platform alerts |
| **Notifications** | Notify | Latest | Multi-platform alerts |
| **Docker Client** | Docker SDK | Latest | Container discovery |
| **Logging** | Logrus + Lumberjack | Latest | Structured logging with rotation |
@@ -1263,8 +1263,8 @@ docker exec charon /app/scripts/restore-backup.sh \
- Future: Dynamic plugin loading for custom providers
2. **Notification Channels:**
- Shoutrrr provides 40+ channels (Discord, Slack, Email, etc.)
- Custom channels via Shoutrrr service URLs
- Notify provides multi-platform channels (Discord, Slack, Gotify, etc.)
- Provider-based configuration with per-channel feature flags
3. **Authentication Providers:**
- Current: Local database authentication

View File

@@ -67,7 +67,7 @@ Before proposing ANY code change or fix, you must build a mental map of the feat
- **Run**: `cd backend && go run ./cmd/api`.
- **Test**: `go test ./...`.
- **Static Analysis (BLOCKING)**: Fast linters run automatically on every commit via pre-commit hooks.
- **Static Analysis (BLOCKING)**: Fast linters run automatically on every commit via lefthook pre-commit-phase hooks.
- **Staticcheck errors MUST be fixed** - commits are BLOCKED until resolved
- Manual run: `make lint-fast` or VS Code task "Lint: Staticcheck (Fast)"
- Staticcheck-only: `make lint-staticcheck-only`
@@ -79,7 +79,7 @@ Before proposing ANY code change or fix, you must build a mental map of the feat
- **Security**: Sanitize all file paths using `filepath.Clean`. Use `fmt.Errorf("context: %w", err)` for error wrapping.
- **Graceful Shutdown**: Long-running work must respect `server.Run(ctx)`.
### Troubleshooting Pre-Commit Staticcheck Failures
### Troubleshooting Lefthook Staticcheck Failures
**Common Issues:**
@@ -175,7 +175,7 @@ Before marking an implementation task as complete, perform the following in orde
- **Exclusions**: Skip this gate for docs-only (`**/*.md`) or frontend-only (`frontend/**`) changes
- **Run One Of**:
- VS Code task: `Lint: GORM Security Scan`
- Pre-commit: `pre-commit run --hook-stage manual gorm-security-scan --all-files`
- Lefthook: `lefthook run pre-commit` (includes gorm-security-scan)
- Direct: `./scripts/scan-gorm-security.sh --check`
- **Gate Enforcement**: DoD is process-blocking until scanner reports zero
CRITICAL/HIGH findings, even while automation remains in manual stage
@@ -189,15 +189,15 @@ Before marking an implementation task as complete, perform the following in orde
- **Expected Behavior**: Report may warn (non-blocking rollout), but artifact generation is mandatory.
3. **Security Scans** (MANDATORY - Zero Tolerance):
- **CodeQL Go Scan**: Run VS Code task "Security: CodeQL Go Scan (CI-Aligned)" OR `pre-commit run codeql-go-scan --all-files`
- **CodeQL Go Scan**: Run VS Code task "Security: CodeQL Go Scan (CI-Aligned)" OR `lefthook run pre-commit`
- Must use `security-and-quality` suite (CI-aligned)
- **Zero high/critical (error-level) findings allowed**
- Medium/low findings should be documented and triaged
- **CodeQL JS Scan**: Run VS Code task "Security: CodeQL JS Scan (CI-Aligned)" OR `pre-commit run codeql-js-scan --all-files`
- **CodeQL JS Scan**: Run VS Code task "Security: CodeQL JS Scan (CI-Aligned)" OR `lefthook run pre-commit`
- Must use `security-and-quality` suite (CI-aligned)
- **Zero high/critical (error-level) findings allowed**
- Medium/low findings should be documented and triaged
- **Validate Findings**: Run `pre-commit run codeql-check-findings --all-files` to check for HIGH/CRITICAL issues
- **Validate Findings**: Run `lefthook run pre-commit` to check for HIGH/CRITICAL issues
- **Trivy Container Scan**: Run VS Code task "Security: Trivy Scan" for container/dependency vulnerabilities
- **Results Viewing**:
- Primary: VS Code SARIF Viewer extension (`MS-SarifVSCode.sarif-viewer`)
@@ -210,7 +210,7 @@ Before marking an implementation task as complete, perform the following in orde
- Database creation: `--threads=0 --overwrite`
- Analysis: `--sarif-add-baseline-file-info`
4. **Pre-Commit Triage**: Run `pre-commit run --all-files`.
4. **Lefthook Triage**: Run `lefthook run pre-commit`.
- If errors occur, **fix them immediately**.
- If logic errors occur, analyze and propose a fix.
- Do not output code that violates pre-commit standards.

View File

@@ -353,7 +353,7 @@ Follow idiomatic Go practices and community standards when writing Go code. Thes
### Development Practices
- Run tests before committing
- Use pre-commit hooks for formatting and linting
- Use lefthook pre-commit-phase hooks for formatting and linting
- Keep commits focused and atomic
- Write meaningful commit messages
- Review diffs before committing

View File

@@ -0,0 +1,204 @@
---
applyTo: SECURITY.md
---
# Instructions: Maintaining `SECURITY.md`
`SECURITY.md` is the project's living security record. It serves two audiences simultaneously: users who need to know what risks exist right now, and the broader community who need confidence that vulnerabilities are being tracked and remediated with discipline. Treat it like a changelog, but for security events — every known issue gets an entry, every resolved issue keeps its entry.
---
## File Structure
`SECURITY.md` must always contain the following top-level sections, in this order:
1. A brief project security policy preamble (responsible disclosure contact, response SLA)
2. **`## Known Vulnerabilities`** — active, unpatched issues
3. **`## Patched Vulnerabilities`** — resolved issues, retained permanently for audit trail
No other top-level sections are required. Do not collapse or remove sections even when they are empty — use the explicit empty-state placeholder defined below.
---
## Section 1: Known Vulnerabilities
This section lists every vulnerability that is currently unpatched or only partially mitigated. Entries must be sorted with the highest severity first, then by discovery date descending within the same severity tier.
### Entry Format
Each entry is an H3 heading followed by a structured block:
```markdown
### [SEVERITY] CVE-XXXX-XXXXX · Short Title
| Field | Value |
|--------------|-------|
| **ID** | CVE-XXXX-XXXXX (or `CHARON-YYYY-NNN` if no CVE assigned yet) |
| **Severity** | Critical / High / Medium / Low · CVSS v3.1 score if known (e.g. `8.1 · High`) |
| **Status** | Investigating / Fix In Progress / Awaiting Upstream / Mitigated (partial) |
**What**
One to three sentences describing the vulnerability class and its impact.
Be specific: name the weakness type (e.g. SQL injection, path traversal, SSRF).
**Who**
- Discovered by: [Reporter name or handle, or "Internal audit", or "Automated scan (tool name)"]
- Reported: YYYY-MM-DD
- Affects: [User roles, API consumers, unauthenticated users, etc.]
**Where**
- Component: [Module or service name]
- File(s): `path/to/affected/file.go`, `path/to/other/file.ts`
- Versions affected: `>= X.Y.Z` (or "all versions" / "prior to X.Y.Z")
**When**
- Discovered: YYYY-MM-DD
- Disclosed (if public): YYYY-MM-DD (or "Not yet publicly disclosed")
- Target fix: YYYY-MM-DD (or sprint/milestone reference)
**How**
A concise technical description of the attack vector, prerequisites, and exploitation
method. Omit proof-of-concept code. Reference CVE advisories or upstream issue
trackers where appropriate.
**Planned Remediation**
Describe the fix strategy: library upgrade, logic refactor, config change, etc.
If a workaround is available in the meantime, document it here.
Link to the tracking issue: [#NNN](https://github.com/owner/repo/issues/NNN)
```
### Empty State
When there are no known vulnerabilities:
```markdown
## Known Vulnerabilities
No known unpatched vulnerabilities at this time.
Last reviewed: YYYY-MM-DD
```
---
## Section 2: Patched Vulnerabilities
This section is a permanent, append-only ledger. Entries are never deleted. Sort newest-patched first. This section builds community trust by demonstrating that issues are resolved promptly and transparently.
### Entry Format
```markdown
### ✅ [SEVERITY] CVE-XXXX-XXXXX · Short Title
| Field | Value |
|--------------|-------|
| **ID** | CVE-XXXX-XXXXX (or internal ID) |
| **Severity** | Critical / High / Medium / Low · CVSS v3.1 score |
| **Patched** | YYYY-MM-DD in `vX.Y.Z` |
**What**
Same description carried over from the Known Vulnerabilities entry.
**Who**
- Discovered by: [Reporter or method]
- Reported: YYYY-MM-DD
**Where**
- Component: [Module or service name]
- File(s): `path/to/affected/file.go`
- Versions affected: `< X.Y.Z`
**When**
- Discovered: YYYY-MM-DD
- Patched: YYYY-MM-DD
- Time to patch: N days
**How**
Same technical description as the original entry.
**Resolution**
Describe exactly what was changed to fix the issue.
- Commit: [`abc1234`](https://github.com/owner/repo/commit/abc1234)
- PR: [#NNN](https://github.com/owner/repo/pull/NNN)
- Release: [`vX.Y.Z`](https://github.com/owner/repo/releases/tag/vX.Y.Z)
**Credit**
[Optional] Thank the reporter if they consented to attribution.
```
### Empty State
```markdown
## Patched Vulnerabilities
No patched vulnerabilities on record yet.
```
---
## Lifecycle: Moving an Entry from Known → Patched
When a fix ships:
1. Remove the entry from `## Known Vulnerabilities` entirely.
2. Add a new entry to the **top** of `## Patched Vulnerabilities` using the patched format above.
3. Carry forward all original fields verbatim — do not rewrite the history of the issue.
4. Add the `**Resolution**` and `**Credit**` blocks with patch details.
5. Update the `Last reviewed` date on the Known Vulnerabilities section if it is now empty.
Do not edit or backfill existing Patched entries once they are committed.
---
## Severity Classification
Use the following definitions consistently:
| Severity | CVSS Range | Meaning |
|----------|------------|---------|
| **Critical** | 9.010.0 | Remote code execution, auth bypass, full data exposure |
| **High** | 7.08.9 | Significant data exposure, privilege escalation, DoS |
| **Medium** | 4.06.9 | Limited data exposure, requires user interaction or auth |
| **Low** | 0.13.9 | Minimal impact, difficult to exploit, defense-in-depth |
When a CVE CVSS score is not yet available, assign a preliminary severity based on these definitions and note it as `(preliminary)` until confirmed.
---
## Internal IDs
If a vulnerability has no CVE assigned, use the format `CHARON-YYYY-NNN` where `YYYY` is the year and `NNN` is a zero-padded sequence number starting at `001` for each year. Example: `CHARON-2025-003`. Assign a CVE ID in the entry retroactively if one is issued later, and add the internal ID as an alias in parentheses.
---
## Responsible Disclosure Preamble
The preamble at the top of `SECURITY.md` (before the vulnerability sections) must include:
- The preferred contact method for reporting vulnerabilities (e.g. a GitHub private advisory link, a security email address, or both)
- An acknowledgment-first response commitment: confirm receipt within 48 hours, even if the full investigation takes longer
- A statement that reporters will not be penalized or publicly named without consent
- A link to the full disclosure policy if one exists
Example:
```markdown
## Reporting a Vulnerability
To report a security issue, please use
[GitHub Private Security Advisories](https://github.com/owner/repo/security/advisories/new)
or email `security@example.com`.
We will acknowledge your report within **48 hours** and provide a remediation
timeline within **7 days**. Reporters are credited with their consent.
We do not pursue legal action against good-faith security researchers.
```
---
## Maintenance Rules
- **Review cadence**: Update the `Last reviewed` date in the Known Vulnerabilities section at least once per release cycle, even if no entries changed.
- **No silent patches**: Every security fix — no matter how minor — must produce an entry in `## Patched Vulnerabilities` before or alongside the release.
- **No redaction**: Do not redact or soften historical entries. Accuracy builds trust; minimizing past issues destroys it.
- **Dependency vulnerabilities**: Transitive dependency CVEs that affect Charon's exposed attack surface must be tracked here the same as first-party vulnerabilities. Pure dev-dependency CVEs with no runtime impact may be omitted at maintainer discretion, but must still be noted in the relevant dependency update PR.
- **Partial mitigations**: If a workaround is deployed but the root cause is not fixed, the entry stays in `## Known Vulnerabilities` with `Status: Mitigated (partial)` and the workaround documented in `**Planned Remediation**`.

View File

@@ -9,7 +9,7 @@ description: 'Repository structure guidelines to maintain organized file placeme
The repository root should contain ONLY:
- Essential config files (`.gitignore`, `.pre-commit-config.yaml`, `Makefile`, etc.)
- Essential config files (`.gitignore`, `Makefile`, etc.)
- Standard project files (`README.md`, `CONTRIBUTING.md`, `LICENSE`, `CHANGELOG.md`)
- Go workspace files (`go.work`, `go.work.sum`)
- VS Code workspace (`Chiron.code-workspace`)

View File

@@ -28,7 +28,7 @@ runSubagent({
- Parallel: run `QA and Security`, `DevOps` and `Doc Writer` in parallel for CI / QA checks and documentation.
- Return: a JSON summary with `subagent_results`, `overall_status`, and aggregated artifacts.
2.1) Multi-PR Slicing Protocol
2.1) Multi-Commit Slicing Protocol
- If a task is large or high-risk, split into PR slices and execute in order.
- Each slice must have:

40
.github/renovate.json vendored
View File

@@ -27,7 +27,10 @@
"rebaseWhen": "auto",
"vulnerabilityAlerts": {
"enabled": true
"enabled": true,
"dependencyDashboardApproval": false,
"automerge": false,
"labels": ["security", "vulnerability"]
},
"rangeStrategy": "bump",
@@ -66,12 +69,45 @@
"description": "Track Alpine base image digest in Dockerfile for security updates",
"managerFilePatterns": ["/^Dockerfile$/"],
"matchStrings": [
"#\\s*renovate:\\s*datasource=docker\\s+depName=alpine.*\\nARG CADDY_IMAGE=alpine:(?<currentValue>[^\\s@]+@sha256:[a-f0-9]+)"
"#\\s*renovate:\\s*datasource=docker\\s+depName=alpine.*\\nARG ALPINE_IMAGE=alpine:(?<currentValue>[^@\\s]+)@(?<currentDigest>sha256:[a-f0-9]+)"
],
"depNameTemplate": "alpine",
"datasourceTemplate": "docker",
"versioningTemplate": "docker"
},
{
"customType": "regex",
"description": "Track Go toolchain version ARG in Dockerfile",
"managerFilePatterns": ["/^Dockerfile$/"],
"matchStrings": [
"#\\s*renovate:\\s*datasource=docker\\s+depName=golang.*\\nARG GO_VERSION=(?<currentValue>[^\\s]+)"
],
"depNameTemplate": "golang",
"datasourceTemplate": "docker",
"versioningTemplate": "docker"
},
{
"customType": "regex",
"description": "Track expr-lang version ARG in Dockerfile",
"managerFilePatterns": ["/^Dockerfile$/"],
"matchStrings": [
"#\\s*renovate:\\s*datasource=go\\s+depName=github\\.com/expr-lang/expr.*\\nARG EXPR_LANG_VERSION=(?<currentValue>[^\\s]+)"
],
"depNameTemplate": "github.com/expr-lang/expr",
"datasourceTemplate": "go",
"versioningTemplate": "semver"
},
{
"customType": "regex",
"description": "Track golang.org/x/net version ARG in Dockerfile",
"managerFilePatterns": ["/^Dockerfile$/"],
"matchStrings": [
"#\\s*renovate:\\s*datasource=go\\s+depName=golang\\.org/x/net.*\\nARG XNET_VERSION=(?<currentValue>[^\\s]+)"
],
"depNameTemplate": "golang.org/x/net",
"datasourceTemplate": "go",
"versioningTemplate": "semver"
},
{
"customType": "regex",
"description": "Track Delve version in Dockerfile",

View File

@@ -63,7 +63,7 @@ Agent Skills are self-documenting, AI-discoverable task definitions that combine
| Skill Name | Category | Description | Status |
|------------|----------|-------------|--------|
| [qa-precommit-all](./qa-precommit-all.SKILL.md) | qa | Run all pre-commit hooks on entire codebase | ✅ Active |
| [qa-lefthook-all](./qa-lefthook-all.SKILL.md) | qa | Run all lefthook pre-commitphase hooks on entire codebase | ✅ Active |
### Utility Skills

View File

@@ -25,7 +25,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version: "1.26.1"
- name: Run GORM Security Scanner
id: gorm-scan

349
.github/skills/qa-lefthook-all.SKILL.md vendored Normal file
View File

@@ -0,0 +1,349 @@
---
# agentskills.io specification v1.0
name: "qa-lefthook-all"
version: "1.0.0"
description: "Run all lefthook pre-commit-phase hooks for comprehensive code quality validation"
author: "Charon Project"
license: "MIT"
tags:
- "qa"
- "quality"
- "pre-commit"
- "linting"
- "validation"
compatibility:
os:
- "linux"
- "darwin"
shells:
- "bash"
requirements:
- name: "python3"
version: ">=3.8"
optional: false
- name: "lefthook"
version: ">=0.14"
optional: false
environment_variables:
- name: "SKIP"
description: "Comma-separated list of hook IDs to skip"
default: ""
required: false
parameters:
- name: "files"
type: "string"
description: "Specific files to check (default: all staged files)"
default: "--all-files"
required: false
outputs:
- name: "validation_report"
type: "stdout"
description: "Results of all pre-commit hook executions"
- name: "exit_code"
type: "number"
description: "0 if all hooks pass, non-zero if any fail"
metadata:
category: "qa"
subcategory: "quality"
execution_time: "medium"
risk_level: "low"
ci_cd_safe: true
requires_network: false
idempotent: true
---
# QA Pre-commit All
## Overview
Executes all configured lefthook pre-commit-phase hooks to validate code quality, formatting, security, and best practices across the entire codebase. This skill runs checks for Python, Go, JavaScript/TypeScript, Markdown, YAML, and more.
This skill is designed for CI/CD pipelines and local quality validation before committing code.
## Prerequisites
- Python 3.8 or higher installed and in PATH
- Python virtual environment activated (`.venv`)
- Pre-commit installed in virtual environment: `pip install pre-commit`
- Pre-commit hooks installed: `pre-commit install`
- All language-specific tools installed (Go, Node.js, etc.)
## Usage
### Basic Usage
Run all pre-commit-phase hooks on all files:
```bash
cd /path/to/charon
lefthook run pre-commit
```
### Staged Files Only
Run lefthook on staged files only (faster):
```bash
lefthook run pre-commit --staged
```
### Specific Hook
Run only a specific hook by ID:
```bash
lefthook run pre-commit --hooks=trailing-whitespace
```
### Skip Specific Hooks
Skip certain hooks during execution:
```bash
SKIP=prettier,eslint .github/skills/scripts/skill-runner.sh qa-precommit-all
```
## Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| files | string | No | --all-files | File selection mode (--all-files or staged) |
## Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| SKIP | No | "" | Comma-separated hook IDs to skip |
| PRE_COMMIT_HOME | No | ~/.cache/pre-commit | Pre-commit cache directory |
## Outputs
- **Success Exit Code**: 0 (all hooks passed)
- **Error Exit Codes**: Non-zero (one or more hooks failed)
- **Output**: Detailed results from each hook
## Pre-commit Hooks Included
The following hooks are configured in `.pre-commit-config.yaml`:
### General Hooks
- **trailing-whitespace**: Remove trailing whitespace
- **end-of-file-fixer**: Ensure files end with newline
- **check-yaml**: Validate YAML syntax
- **check-json**: Validate JSON syntax
- **check-merge-conflict**: Detect merge conflict markers
- **check-added-large-files**: Prevent committing large files
### Python Hooks
- **black**: Code formatting
- **isort**: Import sorting
- **flake8**: Linting
- **mypy**: Type checking
### Go Hooks
- **gofmt**: Code formatting
- **go-vet**: Static analysis
- **golangci-lint**: Comprehensive linting
### JavaScript/TypeScript Hooks
- **prettier**: Code formatting
- **eslint**: Linting and code quality
### Markdown Hooks
- **markdownlint**: Markdown linting and formatting
### Security Hooks
- **detect-private-key**: Prevent committing private keys
- **detect-aws-credentials**: Prevent committing AWS credentials
## Examples
### Example 1: Full Quality Check
```bash
# Run all hooks on all files
source .venv/bin/activate
.github/skills/scripts/skill-runner.sh qa-precommit-all
```
Output:
```
Trim Trailing Whitespace.....................................Passed
Fix End of Files.............................................Passed
Check Yaml...................................................Passed
Check JSON...................................................Passed
Check for merge conflicts....................................Passed
Check for added large files..................................Passed
black........................................................Passed
isort........................................................Passed
prettier.....................................................Passed
eslint.......................................................Passed
markdownlint.................................................Passed
```
### Example 2: Quick Staged Files Check
```bash
# Run only on staged files (faster for pre-commit)
.github/skills/scripts/skill-runner.sh qa-precommit-all staged
```
### Example 3: Skip Slow Hooks
```bash
# Skip time-consuming hooks for quick validation
SKIP=golangci-lint,mypy .github/skills/scripts/skill-runner.sh qa-precommit-all
```
### Example 4: CI/CD Pipeline Integration
```yaml
# GitHub Actions example
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install pre-commit
run: pip install pre-commit
- name: Run QA Pre-commit Checks
run: .github/skills/scripts/skill-runner.sh qa-precommit-all
```
### Example 5: Auto-fix Mode
```bash
# Some hooks can auto-fix issues
# Run twice: first to fix, second to validate
.github/skills/scripts/skill-runner.sh qa-precommit-all || \
.github/skills/scripts/skill-runner.sh qa-precommit-all
```
## Error Handling
### Common Issues
**Virtual environment not activated**:
```bash
Error: pre-commit not found
Solution: source .venv/bin/activate
```
**Pre-commit not installed**:
```bash
Error: pre-commit command not available
Solution: pip install pre-commit
```
**Hooks not installed**:
```bash
Error: Run 'pre-commit install'
Solution: pre-commit install
```
**Hook execution failed**:
```bash
Hook X failed
Solution: Review error output and fix reported issues
```
**Language tool missing**:
```bash
Error: golangci-lint not found
Solution: Install required language tools
```
## Exit Codes
- **0**: All hooks passed
- **1**: One or more hooks failed
- **Other**: Hook execution error
## Hook Fixing Strategies
### Auto-fixable Issues
These hooks automatically fix issues:
- `trailing-whitespace`
- `end-of-file-fixer`
- `black`
- `isort`
- `prettier`
- `gofmt`
**Workflow**: Run pre-commit, review changes, commit fixed files
### Manual Fixes Required
These hooks only report issues:
- `check-yaml`
- `check-json`
- `flake8`
- `eslint`
- `markdownlint`
- `go-vet`
- `golangci-lint`
**Workflow**: Review errors, manually fix code, re-run pre-commit
## Related Skills
- [test-backend-coverage](./test-backend-coverage.SKILL.md) - Backend test coverage
- [test-frontend-coverage](./test-frontend-coverage.SKILL.md) - Frontend test coverage
- [security-scan-trivy](./security-scan-trivy.SKILL.md) - Security scanning
## Notes
- Pre-commit hooks cache their environments for faster execution
- First run may be slow while environments are set up
- Subsequent runs are much faster (seconds vs minutes)
- Hooks run in parallel where possible
- Failed hooks stop execution (fail-fast behavior)
- Use `SKIP` to bypass specific hooks temporarily
- Recommended to run before every commit
- Can be integrated into Git pre-commit hook for automatic checks
- Cache location: `~/.cache/pre-commit` (configurable)
## Performance Tips
- **Initial Setup**: First run takes longer (installing hook environments)
- **Incremental**: Run on staged files only for faster feedback
- **Parallel**: Pre-commit runs compatible hooks in parallel
- **Cache**: Hook environments are cached and reused
- **Skip**: Use `SKIP` to bypass slow hooks during development
## Integration with Git
To automatically run on every commit:
```bash
# Install Git pre-commit hook
pre-commit install
# Now pre-commit runs automatically on git commit
git commit -m "Your commit message"
```
To bypass pre-commit hook temporarily:
```bash
git commit --no-verify -m "Emergency commit"
```
## Configuration
Pre-commit configuration is in `.pre-commit-config.yaml`. To update hooks:
```bash
# Update to latest versions
pre-commit autoupdate
# Clean cache and re-install
pre-commit clean
pre-commit install --install-hooks
```
---
**Last Updated**: 2025-12-20
**Maintained by**: Charon Project
**Source**: `pre-commit run --all-files`

View File

@@ -1,8 +1,8 @@
---
# agentskills.io specification v1.0
name: "qa-precommit-all"
name: "qa-lefthook-all"
version: "1.0.0"
description: "Run all pre-commit hooks for comprehensive code quality validation"
description: "Run all lefthook pre-commit-phase hooks for comprehensive code quality validation"
author: "Charon Project"
license: "MIT"
tags:
@@ -21,15 +21,11 @@ requirements:
- name: "python3"
version: ">=3.8"
optional: false
- name: "pre-commit"
version: ">=2.0"
- name: "lefthook"
version: ">=0.14"
optional: false
environment_variables:
- name: "PRE_COMMIT_HOME"
description: "Pre-commit cache directory"
default: "~/.cache/pre-commit"
required: false
- name: "SKIP"
- name: "SKIP"
description: "Comma-separated list of hook IDs to skip"
default: ""
required: false
@@ -60,7 +56,7 @@ metadata:
## Overview
Executes all configured pre-commit hooks to validate code quality, formatting, security, and best practices across the entire codebase. This skill runs checks for Python, Go, JavaScript/TypeScript, Markdown, YAML, and more.
Executes all configured lefthook pre-commit-phase hooks to validate code quality, formatting, security, and best practices across the entire codebase. This skill runs checks for Python, Go, JavaScript/TypeScript, Markdown, YAML, and more.
This skill is designed for CI/CD pipelines and local quality validation before committing code.
@@ -76,19 +72,19 @@ This skill is designed for CI/CD pipelines and local quality validation before c
### Basic Usage
Run all hooks on all files:
Run all pre-commit-phase hooks on all files:
```bash
cd /path/to/charon
.github/skills/scripts/skill-runner.sh qa-precommit-all
lefthook run pre-commit
```
### Staged Files Only
Run hooks on staged files only (faster):
Run lefthook on staged files only (faster):
```bash
.github/skills/scripts/skill-runner.sh qa-precommit-all staged
lefthook run pre-commit --staged
```
### Specific Hook
@@ -96,7 +92,7 @@ Run hooks on staged files only (faster):
Run only a specific hook by ID:
```bash
SKIP="" .github/skills/scripts/skill-runner.sh qa-precommit-all trailing-whitespace
lefthook run pre-commit --hooks=trailing-whitespace
```
### Skip Specific Hooks

View File

@@ -251,7 +251,7 @@ Solution: Verify source-root points to correct directory
- [security-scan-trivy](./security-scan-trivy.SKILL.md) - Container/dependency vulnerabilities
- [security-scan-go-vuln](./security-scan-go-vuln.SKILL.md) - Go-specific CVE checking
- [qa-precommit-all](./qa-precommit-all.SKILL.md) - Pre-commit quality checks
- [qa-lefthook-all](./qa-lefthook-all.SKILL.md) - Lefthook pre-commit-phase quality checks
## CI Alignment

View File

@@ -35,7 +35,7 @@ fi
# Check Grype
if ! command -v grype >/dev/null 2>&1; then
log_error "Grype not found - install from: https://github.com/anchore/grype"
log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.107.0"
log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.110.0"
error_exit "Grype is required for vulnerability scanning" 2
fi
@@ -50,8 +50,8 @@ SYFT_INSTALLED_VERSION=$(syft version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\
GRYPE_INSTALLED_VERSION=$(grype version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
# Set defaults matching CI workflow
set_default_env "SYFT_VERSION" "v1.17.0"
set_default_env "GRYPE_VERSION" "v0.107.0"
set_default_env "SYFT_VERSION" "v1.42.3"
set_default_env "GRYPE_VERSION" "v0.110.0"
set_default_env "IMAGE_TAG" "charon:local"
set_default_env "FAIL_ON_SEVERITY" "Critical,High"

View File

@@ -545,7 +545,7 @@ Solution: Add suppression comment: // gorm-scanner:ignore [reason]
- [security-scan-trivy](./security-scan-trivy.SKILL.md) - Container vulnerability scanning
- [security-scan-codeql](./security-scan-codeql.SKILL.md) - Static analysis for Go/JS
- [qa-precommit-all](./qa-precommit-all.SKILL.md) - Pre-commit quality checks
- [qa-lefthook-all](./qa-lefthook-all.SKILL.md) - Lefthook pre-commit-phase quality checks
## Best Practices

View File

@@ -227,7 +227,7 @@ Solution: Review and remediate reported vulnerabilities
## Related Skills
- [security-scan-go-vuln](./security-scan-go-vuln.SKILL.md) - Go-specific vulnerability checking
- [qa-precommit-all](./qa-precommit-all.SKILL.md) - Pre-commit quality checks
- [qa-lefthook-all](./qa-lefthook-all.SKILL.md) - Lefthook pre-commit-phase quality checks
## Notes

View File

@@ -21,6 +21,6 @@ jobs:
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Draft Release
uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6
uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -33,7 +33,7 @@ jobs:
- name: Calculate Semantic Version
id: semver
uses: paulhatch/semantic-version@f29500c9d60a99ed5168e39ee367e0976884c46e # v6.0.1
uses: paulhatch/semantic-version@9f72830310d5ed81233b641ee59253644cd8a8fc # v6.0.2
with:
# The prefix to use to create tags
tag_prefix: "v"
@@ -89,7 +89,7 @@ jobs:
- name: Create GitHub Release (creates tag via API)
if: ${{ steps.semver.outputs.changed == 'true' && steps.check_release.outputs.exists == 'false' }}
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
with:
tag_name: ${{ steps.determine_tag.outputs.tag }}
name: Release ${{ steps.determine_tag.outputs.tag }}

View File

@@ -12,7 +12,7 @@ concurrency:
cancel-in-progress: true
env:
GO_VERSION: '1.26.0'
GO_VERSION: '1.26.1'
GOTOOLCHAIN: auto
# Minimal permissions at workflow level; write permissions granted at job level for push only
@@ -38,6 +38,7 @@ jobs:
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Run Benchmark

View File

@@ -31,7 +31,7 @@ jobs:
- name: Build Docker image (Local)
run: |
echo "Building image locally for integration tests..."
docker build -t charon:local .
docker build -t charon:local --build-arg CI="${CI:-false}" .
echo "✅ Successfully built charon:local"
- name: Run Cerberus integration tests

View File

@@ -23,7 +23,7 @@ concurrency:
cancel-in-progress: true
env:
GO_VERSION: '1.26.0'
GO_VERSION: '1.26.1'
NODE_VERSION: '24.12.0'
GOTOOLCHAIN: auto
@@ -48,6 +48,7 @@ jobs:
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
# SECURITY: Keep pull_request (not pull_request_target) for secret-bearing backend tests.
@@ -134,7 +135,7 @@ jobs:
exit "${PIPESTATUS[0]}"
- name: Upload backend coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./backend/coverage.txt
@@ -171,7 +172,7 @@ jobs:
exit "${PIPESTATUS[0]}"
- name: Upload frontend coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./frontend/coverage

View File

@@ -15,6 +15,7 @@ concurrency:
env:
GOTOOLCHAIN: auto
GO_VERSION: '1.26.1'
permissions:
contents: read
@@ -51,7 +52,7 @@ jobs:
run: bash scripts/ci/check-codeql-parity.sh
- name: Initialize CodeQL
uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4
with:
languages: ${{ matrix.language }}
queries: security-and-quality
@@ -64,7 +65,7 @@ jobs:
if: matrix.language == 'go'
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
with:
go-version: 1.26.0
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Verify Go toolchain and build
@@ -91,10 +92,10 @@ jobs:
run: mkdir -p sarif-results
- name: Autobuild
uses: github/codeql-action/autobuild@c793b717bc78562f491db7b0e93a3a178b099162 # v4
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4
with:
category: "/language:${{ matrix.language }}"
output: sarif-results/${{ matrix.language }}

View File

@@ -172,7 +172,7 @@ jobs:
if: always()
steps:
- name: Download all artifacts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: prune-*-log-${{ github.run_id }}
merge-multiple: true

View File

@@ -31,7 +31,7 @@ jobs:
- name: Build Docker image (Local)
run: |
echo "Building image locally for integration tests..."
docker build -t charon:local .
docker build -t charon:local --build-arg CI="${CI:-false}" .
echo "✅ Successfully built charon:local"
- name: Run CrowdSec integration tests

View File

@@ -118,13 +118,14 @@ jobs:
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
if: steps.skip.outputs.skip_build != 'true'
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Resolve Alpine base image digest
if: steps.skip.outputs.skip_build != 'true'
id: caddy
id: alpine
run: |
docker pull alpine:3.23.3
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' alpine:3.23.3)
ALPINE_TAG=$(grep -m1 'ARG ALPINE_IMAGE=' Dockerfile | sed 's/ARG ALPINE_IMAGE=alpine://' | cut -d'@' -f1)
docker pull "alpine:${ALPINE_TAG}"
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "alpine:${ALPINE_TAG}")
echo "image=$DIGEST" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
@@ -199,7 +200,7 @@ jobs:
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: |
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -233,7 +234,7 @@ jobs:
- name: Build and push Docker image (with retry)
if: steps.skip.outputs.skip_build != 'true'
id: build-and-push
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 25
max_attempts: 3
@@ -271,7 +272,7 @@ jobs:
--build-arg "VERSION=${{ steps.meta.outputs.version }}"
--build-arg "BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}"
--build-arg "VCS_REF=${{ env.TRIGGER_HEAD_SHA }}"
--build-arg "CADDY_IMAGE=${{ steps.caddy.outputs.image }}"
--build-arg "ALPINE_IMAGE=${{ steps.alpine.outputs.image }}"
--iidfile /tmp/image-digest.txt
.
)
@@ -531,23 +532,25 @@ jobs:
- name: Run Trivy scan (table output)
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
format: 'table'
severity: 'CRITICAL,HIGH'
exit-code: '0'
version: 'v0.69.3'
continue-on-error: true
- name: Run Trivy vulnerability scanner (SARIF)
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
id: trivy
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
version: 'v0.69.3'
continue-on-error: true
- name: Check Trivy SARIF exists
@@ -562,7 +565,7 @@ jobs:
- name: Upload Trivy results
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true'
uses: github/codeql-action/upload-sarif@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
sarif_file: 'trivy-results.sarif'
category: '.github/workflows/docker-build.yml:build-and-push'
@@ -571,7 +574,7 @@ jobs:
# Generate SBOM (Software Bill of Materials) for supply chain security
# Only for production builds (main/development) - feature branches use downstream supply-chain-pr.yml
- name: Generate SBOM
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
with:
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
@@ -580,7 +583,7 @@ jobs:
# Create verifiable attestation for the SBOM
- name: Attest SBOM
uses: actions/attest-sbom@07e74fc4e78d1aad915e867f9a094073a9f71527 # v4.0.0
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
with:
subject-name: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -591,7 +594,7 @@ jobs:
# Install Cosign for keyless signing
- name: Install Cosign
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
# Sign GHCR image with keyless signing (Sigstore/Fulcio)
- name: Sign GHCR Image
@@ -689,22 +692,24 @@ jobs:
echo "✅ Image freshness validated"
- name: Run Trivy scan on PR image (table output)
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ${{ steps.pr-image.outputs.image_ref }}
format: 'table'
severity: 'CRITICAL,HIGH'
exit-code: '0'
version: 'v0.69.3'
- name: Run Trivy scan on PR image (SARIF - blocking)
id: trivy-scan
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ${{ steps.pr-image.outputs.image_ref }}
format: 'sarif'
output: 'trivy-pr-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Intended to block, but continued on error for now
version: 'v0.69.3'
continue-on-error: true
- name: Check Trivy PR SARIF exists
@@ -719,14 +724,14 @@ jobs:
- name: Upload Trivy scan results
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
uses: github/codeql-action/upload-sarif@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
sarif_file: 'trivy-pr-results.sarif'
category: 'docker-pr-image'
- name: Upload Trivy compatibility results (docker-build category)
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
uses: github/codeql-action/upload-sarif@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
sarif_file: 'trivy-pr-results.sarif'
category: '.github/workflows/docker-build.yml:build-and-push'
@@ -734,7 +739,7 @@ jobs:
- name: Upload Trivy compatibility results (docker-publish alias)
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
uses: github/codeql-action/upload-sarif@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
sarif_file: 'trivy-pr-results.sarif'
category: '.github/workflows/docker-publish.yml:build-and-push'
@@ -742,7 +747,7 @@ jobs:
- name: Upload Trivy compatibility results (nightly alias)
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
uses: github/codeql-action/upload-sarif@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
sarif_file: 'trivy-pr-results.sarif'
category: 'trivy-nightly'

View File

@@ -83,7 +83,7 @@ on:
env:
NODE_VERSION: '20'
GO_VERSION: '1.26.0'
GO_VERSION: '1.26.1'
GOTOOLCHAIN: auto
DOCKERHUB_REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository_owner }}/charon
@@ -145,6 +145,7 @@ jobs:
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
with:
go-version: ${{ env.GO_VERSION }}
cache: true
cache-dependency-path: backend/go.sum
@@ -157,7 +158,7 @@ jobs:
- name: Cache npm dependencies
if: steps.resolve-image.outputs.image_source == 'build'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
@@ -169,12 +170,12 @@ jobs:
- name: Set up Docker Buildx
if: steps.resolve-image.outputs.image_source == 'build'
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Build Docker image
id: build-image
if: steps.resolve-image.outputs.image_source == 'build'
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
context: .
file: ./Dockerfile
@@ -247,7 +248,7 @@ jobs:
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: docker-image
@@ -449,7 +450,7 @@ jobs:
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: docker-image
@@ -659,7 +660,7 @@ jobs:
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: docker-image
@@ -913,7 +914,7 @@ jobs:
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: docker-image
@@ -1150,7 +1151,7 @@ jobs:
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: docker-image
@@ -1395,7 +1396,7 @@ jobs:
- name: Download Docker image artifact
if: needs.build.outputs.image_source == 'build'
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: docker-image

View File

@@ -15,7 +15,7 @@ on:
default: "false"
env:
GO_VERSION: '1.26.0'
GO_VERSION: '1.26.1'
NODE_VERSION: '24.12.0'
GOTOOLCHAIN: auto
GHCR_REGISTRY: ghcr.io
@@ -148,14 +148,13 @@ jobs:
id-token: write
outputs:
version: ${{ steps.meta.outputs.version }}
tags: ${{ steps.meta.outputs.tags }}
digest: ${{ steps.build.outputs.digest }}
digest: ${{ steps.resolve_digest.outputs.digest }}
steps:
- name: Checkout nightly branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: nightly
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
fetch-depth: 0
- name: Set lowercase image name
@@ -165,7 +164,18 @@ jobs:
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Resolve Alpine base image digest
id: alpine
run: |
ALPINE_IMAGE_REF=$(grep -m1 'ARG ALPINE_IMAGE=' Dockerfile | cut -d'=' -f2-)
if [[ -z "$ALPINE_IMAGE_REF" ]]; then
echo "::error::Failed to parse ALPINE_IMAGE from Dockerfile"
exit 1
fi
echo "Resolved Alpine image: ${ALPINE_IMAGE_REF}"
echo "image=${ALPINE_IMAGE_REF}" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
@@ -184,7 +194,7 @@ jobs:
- name: Extract metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: |
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
@@ -199,7 +209,7 @@ jobs:
- name: Build and push Docker image
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -210,22 +220,52 @@ jobs:
VERSION=nightly-${{ github.sha }}
VCS_REF=${{ github.sha }}
BUILD_DATE=${{ github.event.repository.pushed_at }}
ALPINE_IMAGE=${{ steps.alpine.outputs.image }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Resolve and export image digest
id: resolve_digest
run: |
set -euo pipefail
DIGEST="${{ steps.build.outputs.digest }}"
if [[ -z "$DIGEST" ]]; then
echo "Build action digest empty; querying GHCR registry API..."
GHCR_TOKEN=$(curl -sf \
-u "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \
"https://ghcr.io/token?scope=repository:${{ env.IMAGE_NAME }}:pull&service=ghcr.io" \
| jq -r '.token')
DIGEST=$(curl -sfI \
-H "Authorization: Bearer ${GHCR_TOKEN}" \
-H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json" \
"https://ghcr.io/v2/${{ env.IMAGE_NAME }}/manifests/nightly" \
| grep -i '^docker-content-digest:' | awk '{print $2}' | tr -d '\r' || true)
[[ -n "$DIGEST" ]] && echo "Resolved from GHCR API: ${DIGEST}"
fi
if [[ -z "$DIGEST" ]]; then
echo "::error::Could not determine image digest from step output or GHCR registry API"
exit 1
fi
echo "RESOLVED_DIGEST=${DIGEST}" >> "$GITHUB_ENV"
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
echo "Exported digest: ${DIGEST}"
- name: Record nightly image digest
run: |
echo "## 🧾 Nightly Image Digest" >> "$GITHUB_STEP_SUMMARY"
echo "- ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.build.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY"
echo "- ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.resolve_digest.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY"
- name: Generate SBOM
id: sbom_primary
continue-on-error: true
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.build.outputs.digest }}
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}
format: cyclonedx-json
output-file: sbom-nightly.json
syft-version: v1.42.1
@@ -242,7 +282,7 @@ jobs:
echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback"
SYFT_VERSION="v1.42.1"
SYFT_VERSION="v1.42.3"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$ARCH" in
@@ -263,7 +303,12 @@ jobs:
tar -xzf "$TARBALL" syft
chmod +x syft
./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.build.outputs.digest }}" -o cyclonedx-json=sbom-nightly.json
DIGEST="${{ steps.resolve_digest.outputs.digest }}"
if [[ -z "$DIGEST" ]]; then
echo "::error::Digest from resolve_digest step is empty; the digest-resolution step did not complete successfully"
exit 1
fi
./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o cyclonedx-json=sbom-nightly.json
- name: Verify SBOM artifact
if: always()
@@ -288,13 +333,13 @@ jobs:
# Install Cosign for keyless signing
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
# Sign GHCR image with keyless signing (Sigstore/Fulcio)
- name: Sign GHCR Image
run: |
echo "Signing GHCR nightly image with keyless signing..."
cosign sign --yes "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
cosign sign --yes "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
echo "✅ GHCR nightly image signed successfully"
# Sign Docker Hub image with keyless signing (Sigstore/Fulcio)
@@ -302,7 +347,7 @@ jobs:
if: env.HAS_DOCKERHUB_TOKEN == 'true'
run: |
echo "Signing Docker Hub nightly image with keyless signing..."
cosign sign --yes "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
cosign sign --yes "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
echo "✅ Docker Hub nightly image signed successfully"
# Attach SBOM to Docker Hub image
@@ -310,7 +355,7 @@ jobs:
if: env.HAS_DOCKERHUB_TOKEN == 'true'
run: |
echo "Attaching SBOM to Docker Hub nightly image..."
cosign attach sbom --sbom sbom-nightly.json "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
cosign attach sbom --sbom sbom-nightly.json "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
echo "✅ SBOM attached to Docker Hub nightly image"
test-nightly-image:
@@ -324,7 +369,7 @@ jobs:
- name: Checkout nightly branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: nightly
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
- name: Set lowercase image name
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
@@ -341,9 +386,10 @@ jobs:
- name: Run container smoke test
run: |
IMAGE_REF="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}"
docker run --name charon-nightly -d \
-p 8080:8080 \
"${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}"
"${IMAGE_REF}"
# Wait for container to start
sleep 10
@@ -378,32 +424,34 @@ jobs:
- name: Checkout nightly branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: nightly
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
- name: Set lowercase image name
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
- name: Download SBOM
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sbom-nightly
- name: Scan with Grype
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
with:
sbom: sbom-nightly.json
fail-build: false
severity-cutoff: high
- name: Scan with Trivy
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-push-nightly.outputs.digest }}
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}
format: 'sarif'
output: 'trivy-nightly.sarif'
version: 'v0.69.3'
trivyignores: '.trivyignore'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
sarif_file: 'trivy-nightly.sarif'
category: 'trivy-nightly'
@@ -506,18 +554,81 @@ jobs:
echo "- Structured SARIF counts: CRITICAL=${CRITICAL_COUNT}, HIGH=${HIGH_COUNT}, MEDIUM=${MEDIUM_COUNT}"
} >> "$GITHUB_STEP_SUMMARY"
# List all Critical/High/Medium findings with details for triage
# shellcheck disable=SC2016
LIST_FINDINGS='
.runs[] as $run
| ($run.tool.driver.rules // []) as $rules
| $run.results[]?
| . as $result
| (
(
if (($result.ruleIndex | type) == "number") then
($rules[$result.ruleIndex] // {})
else
{}
end
) as $ruleByIndex
| (
[$rules[]? | select((.id // "") == ($result.ruleId // ""))][0] // {}
) as $ruleById
| ($ruleByIndex // $ruleById) as $rule
| ($rule.properties["security-severity"] // null) as $sev
| (try ($sev | tonumber) catch null) as $score
| select($score != null and $score >= 4.0)
| {
id: ($result.ruleId // "unknown"),
score: $score,
severity: (
if $score >= 9.0 then "CRITICAL"
elif $score >= 7.0 then "HIGH"
else "MEDIUM"
end
),
message: ($result.message.text // $rule.shortDescription.text // "no description")[0:120]
}
)
'
echo ""
echo "=== Vulnerability Details ==="
jq -r "[ ${LIST_FINDINGS} ] | sort_by(-.score) | .[] | \"\\(.severity) (\\(.score)): \\(.id) — \\(.message)\"" trivy-nightly.sarif || true
echo "============================="
echo ""
if [ "$CRITICAL_COUNT" -gt 0 ]; then
echo "❌ Critical vulnerabilities found in nightly build (${CRITICAL_COUNT})"
{
echo ""
echo "### ❌ Critical CVEs blocking nightly"
echo '```'
jq -r "[ ${LIST_FINDINGS} | select(.severity == \"CRITICAL\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
if [ "$HIGH_COUNT" -gt 0 ]; then
echo "❌ High vulnerabilities found in nightly build (${HIGH_COUNT})"
{
echo ""
echo "### ❌ High CVEs blocking nightly"
echo '```'
jq -r "[ ${LIST_FINDINGS} | select(.severity == \"HIGH\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
if [ "$MEDIUM_COUNT" -gt 0 ]; then
echo "::warning::Medium vulnerabilities found in nightly build (${MEDIUM_COUNT}). Non-blocking by policy; triage with SLA per .github/security-severity-policy.yml"
{
echo ""
echo "### ⚠️ Medium CVEs (non-blocking)"
echo '```'
jq -r "[ ${LIST_FINDINGS} | select(.severity == \"MEDIUM\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
fi
echo "✅ No Critical/High vulnerabilities found"

View File

@@ -16,7 +16,7 @@ permissions:
checks: write
env:
GO_VERSION: '1.26.0'
GO_VERSION: '1.26.1'
NODE_VERSION: '24.12.0'
GOTOOLCHAIN: auto
@@ -34,6 +34,7 @@ jobs:
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Run auth protection contract tests
@@ -140,6 +141,7 @@ jobs:
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Repo health check
@@ -152,8 +154,7 @@ jobs:
env:
CGO_ENABLED: 1
run: |
bash "scripts/go-test-coverage.sh" 2>&1 | tee backend/test-output.txt
exit "${PIPESTATUS[0]}"
bash "scripts/go-test-coverage.sh" 2>&1 | tee backend/test-output.txt; exit "${PIPESTATUS[0]}"
- name: Go Test Summary
if: always()
@@ -230,11 +231,12 @@ jobs:
PERF_MAX_MS_GETSTATUS_P95_PARALLEL: 1500ms
PERF_MAX_MS_LISTDECISIONS_P95: 2000ms
run: |
go test -run TestPerf -v ./internal/api/handlers -count=1 2>&1 | tee perf-output.txt; PERF_STATUS="${PIPESTATUS[0]}"
{
echo "## 🔍 Running performance assertions (TestPerf)"
go test -run TestPerf -v ./internal/api/handlers -count=1 | tee perf-output.txt
cat perf-output.txt
} >> "$GITHUB_STEP_SUMMARY"
exit "${PIPESTATUS[0]}"
exit "$PERF_STATUS"
frontend-quality:
name: Frontend (React)
@@ -296,8 +298,7 @@ jobs:
id: frontend-tests
working-directory: ${{ github.workspace }}
run: |
bash scripts/frontend-test-coverage.sh 2>&1 | tee frontend/test-output.txt
exit "${PIPESTATUS[0]}"
bash scripts/frontend-test-coverage.sh 2>&1 | tee frontend/test-output.txt; exit "${PIPESTATUS[0]}"
- name: Frontend Test Summary
if: always()

View File

@@ -31,7 +31,7 @@ jobs:
- name: Build Docker image (Local)
run: |
echo "Building image locally for integration tests..."
docker build -t charon:local .
docker build -t charon:local --build-arg CI="${CI:-false}" .
echo "✅ Successfully built charon:local"
- name: Run rate limit integration tests
@@ -68,7 +68,7 @@ jobs:
echo "### Caddy Admin Config (rate_limit handlers)"
echo '```json'
curl -s http://localhost:2119/config 2>/dev/null | grep -A 20 '"handler":"rate_limit"' | head -30 || echo "Could not retrieve Caddy config"
curl -s http://localhost:2119/config/ 2>/dev/null | grep -A 20 '"handler":"rate_limit"' | head -30 || echo "Could not retrieve Caddy config"
echo '```'
echo ""

View File

@@ -10,7 +10,7 @@ concurrency:
cancel-in-progress: false
env:
GO_VERSION: '1.26.0'
GO_VERSION: '1.26.1'
NODE_VERSION: '24.12.0'
GOTOOLCHAIN: auto
@@ -48,6 +48,7 @@ jobs:
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: backend/go.sum
- name: Set up Node.js

View File

@@ -25,7 +25,7 @@ jobs:
fetch-depth: 1
- name: Run Renovate
uses: renovatebot/github-action@7b4b65bf31e07d4e3e51708d07700fb41bc03166 # v46.1.3
uses: renovatebot/github-action@abd08c7549b2a864af5df4a2e369c43f035a6a9d # v46.1.5
with:
configurationFile: .github/renovate.json
token: ${{ secrets.RENOVATE_TOKEN || secrets.GITHUB_TOKEN }}

View File

@@ -240,7 +240,7 @@ jobs:
- name: Download PR image artifact
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
# actions/download-artifact v4.1.8
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3
uses: actions/download-artifact@484a0b528fb4d7bd804637ccb632e47a0e638317
with:
name: ${{ steps.check-artifact.outputs.artifact_name }}
run-id: ${{ steps.check-artifact.outputs.run_id }}
@@ -385,7 +385,7 @@ jobs:
- name: Upload Trivy SARIF to GitHub Security
if: always() && steps.trivy-sarif-check.outputs.exists == 'true'
# github/codeql-action v4
uses: github/codeql-action/upload-sarif@a5b959e10d29aec4f277040b4d27d0f6bea2322a
uses: github/codeql-action/upload-sarif@05b1a5d28f8763fd11e77388fe57846f1ba8e766
with:
sarif_file: 'trivy-binary-results.sarif'
category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }}

View File

@@ -50,7 +50,7 @@ jobs:
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Resolve Debian base image digest
id: base-image
@@ -69,7 +69,7 @@ jobs:
- name: Extract metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@@ -77,7 +77,7 @@ jobs:
- name: Build Docker image (NO CACHE)
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with:
context: .
platforms: linux/amd64
@@ -93,35 +93,38 @@ jobs:
BASE_IMAGE=${{ steps.base-image.outputs.digest }}
- name: Run Trivy vulnerability scanner (CRITICAL+HIGH)
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: 'table'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail workflow if vulnerabilities found
version: 'v0.69.3'
continue-on-error: true
- name: Run Trivy vulnerability scanner (SARIF)
id: trivy-sarif
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: 'sarif'
output: 'trivy-weekly-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
version: 'v0.69.3'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
with:
sarif_file: 'trivy-weekly-results.sarif'
- name: Run Trivy vulnerability scanner (JSON for artifact)
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # 0.34.2
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: 'json'
output: 'trivy-weekly-results.json'
severity: 'CRITICAL,HIGH,MEDIUM,LOW'
version: 'v0.69.3'
- name: Upload Trivy JSON results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7

View File

@@ -266,7 +266,7 @@ jobs:
# Generate SBOM using official Anchore action (auto-updated by Renovate)
- name: Generate SBOM
if: steps.set-target.outputs.image_name != ''
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
id: sbom
with:
image: ${{ steps.set-target.outputs.image_name }}
@@ -285,7 +285,7 @@ jobs:
- name: Install Grype
if: steps.set-target.outputs.image_name != ''
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.107.1
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.110.0
- name: Scan for vulnerabilities
if: steps.set-target.outputs.image_name != ''
@@ -362,7 +362,7 @@ jobs:
- name: Upload SARIF to GitHub Security
if: steps.check-artifact.outputs.artifact_found == 'true'
uses: github/codeql-action/upload-sarif@c793b717bc78562f491db7b0e93a3a178b099162 # v4
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4
continue-on-error: true
with:
sarif_file: grype-results.sarif
@@ -381,9 +381,12 @@ jobs:
- name: Comment on PR
if: steps.set-target.outputs.image_name != '' && steps.pr-number.outputs.is_push != 'true' && steps.pr-number.outputs.pr_number != ''
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
PR_NUMBER="${{ steps.pr-number.outputs.pr_number }}"
COMPONENT_COUNT="${{ steps.sbom-count.outputs.component_count }}"
CRITICAL_COUNT="${{ steps.vuln-summary.outputs.critical_count }}"
@@ -429,29 +432,38 @@ jobs:
EOF
)
# Find and update existing comment or create new one
COMMENT_ID=$(gh api \
# Fetch existing comments — skip gracefully on 403 / permission errors
COMMENTS_JSON=""
if ! COMMENTS_JSON=$(gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
--jq '.[] | select(.body | contains("Supply Chain Verification Results")) | .id' | head -1)
"/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" 2>/dev/null); then
echo "⚠️ Cannot access PR comments (likely token permissions / fork / event context). Skipping PR comment."
exit 0
fi
if [[ -n "${COMMENT_ID}" ]]; then
COMMENT_ID=$(echo "${COMMENTS_JSON}" | jq -r '.[] | select(.body | contains("Supply Chain Verification Results")) | .id' | head -1)
if [[ -n "${COMMENT_ID:-}" && "${COMMENT_ID}" != "null" ]]; then
echo "📝 Updating existing comment..."
gh api \
--method PATCH \
if ! gh api --method PATCH \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
-f body="${COMMENT_BODY}"
-f body="${COMMENT_BODY}"; then
echo "⚠️ Failed to update comment (permissions?). Skipping."
exit 0
fi
else
echo "📝 Creating new comment..."
gh api \
--method POST \
if ! gh api --method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
-f body="${COMMENT_BODY}"
-f body="${COMMENT_BODY}"; then
echo "⚠️ Failed to create comment (permissions?). Skipping."
exit 0
fi
fi
echo "✅ PR comment posted"

View File

@@ -119,7 +119,7 @@ jobs:
# Generate SBOM using official Anchore action (auto-updated by Renovate)
- name: Generate and Verify SBOM
if: steps.image-check.outputs.exists == 'true'
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
image: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
format: cyclonedx-json
@@ -233,7 +233,7 @@ jobs:
# Scan for vulnerabilities using official Anchore action (auto-updated by Renovate)
- name: Scan for Vulnerabilities
if: steps.validate-sbom.outputs.valid == 'true'
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
id: scan
with:
sbom: sbom-verify.cyclonedx.json

View File

@@ -31,7 +31,7 @@ jobs:
- name: Build Docker image (Local)
run: |
echo "Building image locally for integration tests..."
docker build -t charon:local .
docker build -t charon:local --build-arg CI="${CI:-false}" .
echo "✅ Successfully built charon:local"
- name: Run WAF integration tests

View File

@@ -200,8 +200,8 @@ jobs:
runs-on: ubuntu-latest
if: needs.check-nightly-health.outputs.is_healthy == 'true'
outputs:
pr_number: ${{ steps.create-pr.outputs.pr_number }}
pr_url: ${{ steps.create-pr.outputs.pr_url }}
pr_number: ${{ steps.create-pr.outputs.pr_number || steps.existing-pr.outputs.pr_number }}
pr_url: ${{ steps.create-pr.outputs.pr_url || steps.existing-pr.outputs.pr_url }}
skipped: ${{ steps.check-diff.outputs.skipped }}
steps:

6
.gitignore vendored
View File

@@ -78,6 +78,11 @@ backend/node_modules/
backend/package.json
backend/package-lock.json
# Root-level artifact files (non-documentation)
FIREFOX_E2E_FIXES_SUMMARY.md
verify-security-state-for-ui-tests
categories.txt
# -----------------------------------------------------------------------------
# Databases
# -----------------------------------------------------------------------------
@@ -297,6 +302,7 @@ docs/plans/current_spec_notes.md
tests/etc/passwd
trivy-image-report.json
trivy-fs-report.json
trivy-report.json
backend/# Tools Configuration.md
docs/plans/requirements.md
docs/plans/design.md

View File

@@ -4,61 +4,6 @@
# Documentation: https://github.com/anchore/grype#specifying-matches-to-ignore
ignore:
# CVE-2026-22184: zlib Global Buffer Overflow in untgz utility
# Severity: CRITICAL
# Package: zlib 1.3.1-r2 (Alpine Linux base image)
# Status: No upstream fix available as of 2026-01-16
#
# Vulnerability Details:
# - Global buffer overflow in TGZfname() function
# - Unbounded strcpy() allows attacker-controlled archive names
# - Can lead to memory corruption, DoS, potential RCE
#
# Risk Assessment: ACCEPTED (Low exploitability in Charon context)
# - Charon does not use untgz utility directly
# - No untrusted tar archive processing in application code
# - Attack surface limited to OS-level utilities
# - Multiple layers of containerization and isolation
#
# Mitigation:
# - Monitor Alpine Linux security feed daily for zlib patches
# - Container runs with minimal privileges (no-new-privileges)
# - Read-only filesystem where possible
# - Network isolation via Docker networks
#
# Review:
# - Daily checks for Alpine security updates
# - Automatic re-scan via CI/CD on every commit
# - Manual review scheduled for 2026-01-23 (7 days)
#
# Removal Criteria:
# - Alpine releases zlib 1.3.1-r3 or higher with CVE fix
# - OR upstream zlib project releases patched version
# - Remove this suppression immediately after fix available
#
# References:
# - CVE: https://nvd.nist.gov/vuln/detail/CVE-2026-22184
# - Alpine Security: https://security.alpinelinux.org/
# - GitHub Issue: https://github.com/Wikid82/Charon/issues/TBD
- vulnerability: CVE-2026-22184
package:
name: zlib
version: "1.3.1-r2"
type: apk # Alpine package
reason: |
CRITICAL buffer overflow in untgz utility. No fix available from Alpine
as of 2026-01-16. Risk accepted: Charon does not directly use untgz or
process untrusted tar archives. Attack surface limited to base OS utilities.
Monitoring Alpine security feed for upstream patch.
expiry: "2026-01-23" # Re-evaluate in 7 days
# Action items when this suppression expires:
# 1. Check Alpine security feed: https://security.alpinelinux.org/
# 2. Check zlib releases: https://github.com/madler/zlib/releases
# 3. If fix available: Update Dockerfile, rebuild, remove suppression
# 4. If no fix: Extend expiry by 7 days, document justification
# 5. If extended 3+ times: Escalate to security team for review
# GHSA-69x3-g4r3-p962 / CVE-2026-25793: Nebula ECDSA Signature Malleability
# Severity: HIGH (CVSS 8.1)
# Package: github.com/slackhq/nebula v1.9.7 (embedded in /usr/bin/caddy)
@@ -98,7 +43,8 @@ ignore:
# Review:
# - Reviewed 2026-02-19: smallstep/certificates latest stable remains v0.27.5;
# no release requiring nebula v1.10+ has shipped. Suppression extended 14 days.
# - Next review: 2026-03-05. Remove suppression immediately once upstream fixes.
# - Reviewed 2026-03-13: smallstep/certificates stable still v0.27.5, extended 30 days.
# - Next review: 2026-04-12. Remove suppression immediately once upstream fixes.
#
# Removal Criteria:
# - smallstep/certificates releases a stable version requiring nebula v1.10+
@@ -118,11 +64,11 @@ ignore:
type: go-module
reason: |
HIGH — ECDSA signature malleability in nebula v1.9.7 embedded in /usr/bin/caddy.
Cannot upgrade: smallstep/certificates v0.27.5 (latest stable as of 2026-02-19)
Cannot upgrade: smallstep/certificates v0.27.5 (latest stable as of 2026-03-13)
still requires nebula v1.9.x (verified across v0.27.5v0.30.0-rc2). Charon does
not use Nebula VPN PKI by default. Risk accepted pending upstream smallstep fix.
Reviewed 2026-02-19: no new smallstep release changes this assessment.
expiry: "2026-03-05" # Re-evaluate in 14 days (2026-02-19 + 14 days)
Reviewed 2026-03-13: smallstep/certificates stable still v0.27.5, extended 30 days.
expiry: "2026-04-12" # Re-evaluated 2026-03-13: smallstep/certificates stable still v0.27.5, extended 30 days.
# Action items when this suppression expires:
# 1. Check smallstep/certificates releases: https://github.com/smallstep/certificates/releases
@@ -135,6 +81,441 @@ ignore:
# 3. If no fix yet: Extend expiry by 14 days and document justification
# 4. If extended 3+ times: Open upstream issue on smallstep/certificates
# CVE-2026-2673: OpenSSL TLS 1.3 server key exchange group downgrade
# Severity: HIGH (CVSS 7.5)
# Packages: libcrypto3 3.5.5-r0 and libssl3 3.5.5-r0 (Alpine apk)
# Status: No upstream fix available — Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-03-18
#
# Vulnerability Details:
# - When DEFAULT is in the TLS 1.3 group configuration, the OpenSSL server may select
# a weaker key exchange group than preferred, enabling a limited key exchange downgrade.
# - Only affects systems acting as a raw TLS 1.3 server using OpenSSL's server-side group negotiation.
#
# Root Cause (No Fix Available):
# - Alpine upstream has not published a patched libcrypto3/libssl3 for Alpine 3.23.
# - Checked: Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-03-18.
# - Fix path: once Alpine publishes a patched libcrypto3/libssl3, rebuild the Docker image
# and remove this suppression.
#
# Risk Assessment: ACCEPTED (No upstream fix; limited exposure in Charon context)
# - Charon terminates TLS at the Caddy layer — the Go backend does not act as a raw TLS 1.3 server.
# - The vulnerability requires the affected application to directly configure TLS 1.3 server
# group negotiation via OpenSSL, which Charon does not do.
# - Container-level isolation reduces the attack surface further.
#
# Mitigation (active while suppression is in effect):
# - Monitor Alpine security advisories: https://security.alpinelinux.org/vuln/CVE-2026-2673
# - Weekly CI security rebuild (security-weekly-rebuild.yml) flags any new CVEs in the full image.
#
# Review:
# - Reviewed 2026-03-18 (initial suppression): no upstream fix available. Set 30-day review.
# - Next review: 2026-04-18. Remove suppression immediately once upstream fixes.
#
# Removal Criteria:
# - Alpine publishes a patched version of libcrypto3 and libssl3
# - Rebuild Docker image and verify CVE-2026-2673 no longer appears in grype-results.json
# - Remove both these entries and the corresponding .trivyignore entry simultaneously
#
# References:
# - CVE-2026-2673: https://nvd.nist.gov/vuln/detail/CVE-2026-2673
# - Alpine security tracker: https://security.alpinelinux.org/vuln/CVE-2026-2673
- vulnerability: CVE-2026-2673
package:
name: libcrypto3
version: "3.5.5-r0"
type: apk
reason: |
HIGH — OpenSSL TLS 1.3 server key exchange group downgrade in libcrypto3 3.5.5-r0 (Alpine base image).
No upstream fix: Alpine 3.23 still ships libcrypto3 3.5.5-r0 as of 2026-03-18. Charon
terminates TLS at the Caddy layer; the Go backend does not act as a raw TLS 1.3 server.
Risk accepted pending Alpine upstream patch.
expiry: "2026-04-18" # Initial 30-day review period. Extend in 1430 day increments with documented justification.
# Action items when this suppression expires:
# 1. Check Alpine security tracker: https://security.alpinelinux.org/vuln/CVE-2026-2673
# 2. If a patched Alpine package is now available:
# a. Rebuild Docker image without suppression
# b. Run local security-scan-docker-image and confirm CVE is resolved
# c. Remove this suppression entry, the libssl3 entry below, and the .trivyignore entry
# 3. If no fix yet: Extend expiry by 1430 days and update the review comment above
# 4. If extended 3+ times: Open an issue to track the upstream status formally
# CVE-2026-2673 (libssl3) — see full justification in the libcrypto3 entry above
- vulnerability: CVE-2026-2673
package:
name: libssl3
version: "3.5.5-r0"
type: apk
reason: |
HIGH — OpenSSL TLS 1.3 server key exchange group downgrade in libssl3 3.5.5-r0 (Alpine base image).
No upstream fix: Alpine 3.23 still ships libssl3 3.5.5-r0 as of 2026-03-18. Charon
terminates TLS at the Caddy layer; the Go backend does not act as a raw TLS 1.3 server.
Risk accepted pending Alpine upstream patch.
expiry: "2026-04-18" # Initial 30-day review period. See libcrypto3 entry above for action items.
# CVE-2026-33186 / GHSA-p77j-4mvh-x3m3: gRPC-Go authorization bypass via missing leading slash
# Severity: CRITICAL (CVSS 9.1)
# Package: google.golang.org/grpc v1.74.2 (embedded in /usr/local/bin/crowdsec and /usr/local/bin/cscli)
# Status: Fix available at v1.79.3 — waiting on CrowdSec upstream to release with patched grpc
#
# Vulnerability Details:
# - gRPC-Go server path-based authorization (grpc/authz) fails to match deny rules when
# the HTTP/2 :path pseudo-header is missing its leading slash (e.g., "Service/Method"
# instead of "/Service/Method"), allowing a fallback allow-rule to grant access instead.
# - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
#
# Root Cause (Third-Party Binary):
# - Charon's own grpc dependency is patched to v1.79.3 (updated 2026-03-19).
# - CrowdSec ships grpc v1.74.2 compiled into its binary; Charon has no control over this.
# - This is a server-side vulnerability. CrowdSec uses grpc as a server; Charon uses it
# only as a client (via the Docker SDK). CrowdSec's internal grpc server is not exposed
# to external traffic in a standard Charon deployment.
# - Fix path: once CrowdSec releases a version built with grpc >= v1.79.3, rebuild the
# Docker image (Renovate tracks the CrowdSec version) and remove this suppression.
#
# Risk Assessment: ACCEPTED (Constrained exploitability in Charon context)
# - The vulnerable code path requires an attacker to reach CrowdSec's internal grpc server,
# which is bound to localhost/internal interfaces in the Charon container network.
# - Container-level isolation (no exposed grpc port) significantly limits exposure.
# - Charon does not configure grpc/authz deny rules on CrowdSec's server.
#
# Mitigation (active while suppression is in effect):
# - Monitor CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
# - Weekly CI security rebuild flags the moment a fixed CrowdSec image ships.
#
# Review:
# - Reviewed 2026-03-19 (initial suppression): grpc v1.79.3 fix exists; CrowdSec has not
# yet shipped an updated release. Suppression set for 14-day review given fix availability.
# - Next review: 2026-04-02. Remove suppression once CrowdSec ships with grpc >= v1.79.3.
#
# Removal Criteria:
# - CrowdSec releases a version built with google.golang.org/grpc >= v1.79.3
# - Rebuild Docker image, run security-scan-docker-image, confirm finding is resolved
# - Remove this entry and the corresponding .trivyignore entry simultaneously
#
# References:
# - GHSA-p77j-4mvh-x3m3: https://github.com/advisories/GHSA-p77j-4mvh-x3m3
# - CVE-2026-33186: https://nvd.nist.gov/vuln/detail/CVE-2026-33186
# - grpc fix (v1.79.3): https://github.com/grpc/grpc-go/releases/tag/v1.79.3
# - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
- vulnerability: CVE-2026-33186
package:
name: google.golang.org/grpc
version: "v1.74.2"
type: go-module
reason: |
CRITICAL — gRPC-Go authorization bypass in grpc v1.74.2 embedded in /usr/local/bin/crowdsec
and /usr/local/bin/cscli. Fix available at v1.79.3 (Charon's own dep is patched); waiting
on CrowdSec upstream to release with patched grpc. CrowdSec's grpc server is not exposed
externally in a standard Charon deployment. Risk accepted pending CrowdSec upstream fix.
Reviewed 2026-03-19: CrowdSec has not yet released with grpc >= v1.79.3.
expiry: "2026-04-02" # 14-day review: fix exists at v1.79.3; check CrowdSec releases.
# Action items when this suppression expires:
# 1. Check CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
# 2. If CrowdSec ships with grpc >= v1.79.3:
# a. Renovate should auto-PR the new CrowdSec version in the Dockerfile
# b. Merge the Renovate PR, rebuild Docker image
# c. Run local security-scan-docker-image and confirm grpc v1.74.2 is gone
# d. Remove this suppression entry and the corresponding .trivyignore entry
# 3. If no fix yet: Extend expiry by 14 days and document justification
# 4. If extended 3+ times: Open an upstream issue on crowdsecurity/crowdsec
# CVE-2026-33186 (Caddy) — see full justification in the CrowdSec entry above
# Package: google.golang.org/grpc v1.79.1 (embedded in /usr/bin/caddy)
# Status: Fix available at v1.79.3 — waiting on a new Caddy release built with patched grpc
- vulnerability: CVE-2026-33186
package:
name: google.golang.org/grpc
version: "v1.79.1"
type: go-module
reason: |
CRITICAL — gRPC-Go authorization bypass in grpc v1.79.1 embedded in /usr/bin/caddy.
Fix available at v1.79.3; waiting on Caddy upstream to release a build with patched grpc.
Caddy's grpc server is not exposed externally in a standard Charon deployment.
Risk accepted pending Caddy upstream fix. Reviewed 2026-03-19: no Caddy release with grpc >= v1.79.3 yet.
expiry: "2026-04-02" # 14-day review: fix exists at v1.79.3; check Caddy releases.
# Action items when this suppression expires:
# 1. Check Caddy releases: https://github.com/caddyserver/caddy/releases
# (or the custom caddy-builder in the Dockerfile for caddy-security plugin)
# 2. If a new Caddy build ships with grpc >= v1.79.3:
# a. Update the Caddy version pin in the Dockerfile caddy-builder stage
# b. Rebuild Docker image and run local security-scan-docker-image
# c. Remove this suppression entry and the corresponding .trivyignore entry
# 3. If no fix yet: Extend expiry by 14 days and document justification
# 4. If extended 3+ times: Open an issue on caddyserver/caddy
# GHSA-479m-364c-43vc: goxmldsig XML signature validation bypass (loop variable capture)
# Severity: HIGH (CVSS 7.5)
# Package: github.com/russellhaering/goxmldsig v1.5.0 (embedded in /usr/bin/caddy)
# Status: Fix available at v1.6.0 — waiting on a new Caddy release built with patched goxmldsig
#
# Vulnerability Details:
# - Loop variable capture in validateSignature causes the signature reference to always
# point to the last element in SignedInfo.References; an attacker can substitute signed
# element content and bypass XML signature integrity validation (CWE-347, CWE-682).
# - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
#
# Root Cause (Third-Party Binary):
# - Charon does not use goxmldsig directly. The package is compiled into /usr/bin/caddy
# via the caddy-security plugin's SAML/SSO support.
# - Fix path: once Caddy (or the caddy-security plugin) releases a build with
# goxmldsig >= v1.6.0, rebuild the Docker image and remove this suppression.
#
# Risk Assessment: ACCEPTED (Low exploitability in default Charon context)
# - The vulnerability only affects SAML/XML signature validation workflows.
# - Charon does not enable or configure SAML-based SSO in its default setup.
# - Exploiting this requires an active SAML integration, which is non-default.
#
# Mitigation (active while suppression is in effect):
# - Monitor caddy-security plugin releases: https://github.com/greenpau/caddy-security/releases
# - Monitor Caddy releases: https://github.com/caddyserver/caddy/releases
# - Weekly CI security rebuild flags the moment a fixed image ships.
#
# Review:
# - Reviewed 2026-03-19 (initial suppression): goxmldsig v1.6.0 fix exists; Caddy has not
# yet shipped with the updated dep. Set 14-day review given fix availability.
# - Next review: 2026-04-02. Remove suppression once Caddy ships with goxmldsig >= v1.6.0.
#
# Removal Criteria:
# - Caddy (or caddy-security plugin) releases a build with goxmldsig >= v1.6.0
# - Rebuild Docker image, run security-scan-docker-image, confirm finding is resolved
# - Remove this entry and the corresponding .trivyignore entry simultaneously
#
# References:
# - GHSA-479m-364c-43vc: https://github.com/advisories/GHSA-479m-364c-43vc
# - goxmldsig v1.6.0 fix: https://github.com/russellhaering/goxmldsig/releases/tag/v1.6.0
# - caddy-security plugin: https://github.com/greenpau/caddy-security/releases
- vulnerability: GHSA-479m-364c-43vc
package:
name: github.com/russellhaering/goxmldsig
version: "v1.5.0"
type: go-module
reason: |
HIGH — XML signature validation bypass in goxmldsig v1.5.0 embedded in /usr/bin/caddy.
Fix available at v1.6.0; waiting on Caddy upstream to release a build with patched goxmldsig.
Charon does not configure SAML-based SSO by default; the vulnerable XML signature path
is not reachable in a standard deployment. Risk accepted pending Caddy upstream fix.
Reviewed 2026-03-19: no Caddy release with goxmldsig >= v1.6.0 yet.
expiry: "2026-04-02" # 14-day review: fix exists at v1.6.0; check Caddy/caddy-security releases.
# Action items when this suppression expires:
# 1. Check caddy-security releases: https://github.com/greenpau/caddy-security/releases
# 2. If a new build ships with goxmldsig >= v1.6.0:
# a. Update the Caddy version pin in the Dockerfile caddy-builder stage if needed
# b. Rebuild Docker image and run local security-scan-docker-image
# c. Remove this suppression entry and the corresponding .trivyignore entry
# 3. If no fix yet: Extend expiry by 14 days and document justification
# GHSA-6g7g-w4f8-9c9x: buger/jsonparser Delete panic on malformed JSON (DoS)
# Severity: HIGH (CVSS 7.5)
# Package: github.com/buger/jsonparser v1.1.1 (embedded in /usr/local/bin/crowdsec and /usr/local/bin/cscli)
# Status: NO upstream fix available — OSV marks "Last affected: v1.1.1" with no Fixed event
#
# Vulnerability Details:
# - The Delete function fails to validate offsets on malformed JSON input, producing a
# negative slice index and a runtime panic — denial of service (CWE-125).
# - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
#
# Root Cause (Third-Party Binary + No Upstream Fix):
# - Charon does not use buger/jsonparser directly. It is compiled into CrowdSec binaries.
# - The buger/jsonparser repository has no released fix as of 2026-03-19 (GitHub issue #275
# and golang/vulndb #4514 are both open).
# - Fix path: once buger/jsonparser releases a patched version and CrowdSec updates their
# dependency, rebuild the Docker image and remove this suppression.
#
# Risk Assessment: ACCEPTED (Limited exploitability + no upstream fix)
# - The DoS vector requires passing malformed JSON to the vulnerable Delete function within
# CrowdSec's internal processing pipeline; this is not a direct attack surface in Charon.
# - CrowdSec's exposed surface is its HTTP API (not raw JSON stream parsing via this path).
#
# Mitigation (active while suppression is in effect):
# - Monitor buger/jsonparser: https://github.com/buger/jsonparser/issues/275
# - Monitor CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
# - Weekly CI security rebuild flags the moment a fixed image ships.
#
# Review:
# - Reviewed 2026-03-19 (initial suppression): no upstream fix exists. Set 30-day review.
# - Next review: 2026-04-19. Remove suppression once buger/jsonparser ships a fix and
# CrowdSec updates their dependency.
#
# Removal Criteria:
# - buger/jsonparser releases a patched version (v1.1.2 or higher)
# - CrowdSec releases a version built with the patched jsonparser
# - Rebuild Docker image, run security-scan-docker-image, confirm finding is resolved
# - Remove this entry and the corresponding .trivyignore entry simultaneously
#
# References:
# - GHSA-6g7g-w4f8-9c9x: https://github.com/advisories/GHSA-6g7g-w4f8-9c9x
# - Upstream issue: https://github.com/buger/jsonparser/issues/275
# - golang/vulndb: https://github.com/golang/vulndb/issues/4514
# - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
- vulnerability: GHSA-6g7g-w4f8-9c9x
package:
name: github.com/buger/jsonparser
version: "v1.1.1"
type: go-module
reason: |
HIGH — DoS panic via malformed JSON in buger/jsonparser v1.1.1 embedded in CrowdSec binaries.
No upstream fix: buger/jsonparser has no released patch as of 2026-03-19 (issue #275 open).
Charon does not use this package directly; the vector requires reaching CrowdSec's internal
JSON processing pipeline. Risk accepted; no remediation path until upstream ships a fix.
Reviewed 2026-03-19: no patched release available.
expiry: "2026-04-19" # 30-day review: no fix exists. Extend in 30-day increments with documented justification.
# Action items when this suppression expires:
# 1. Check buger/jsonparser releases: https://github.com/buger/jsonparser/releases
# and issue #275: https://github.com/buger/jsonparser/issues/275
# 2. If a fix has shipped AND CrowdSec has updated their dependency:
# a. Rebuild Docker image and run local security-scan-docker-image
# b. Remove this suppression entry and the corresponding .trivyignore entry
# 3. If no fix yet: Extend expiry by 30 days and update the review comment above
# 4. If extended 3+ times with no progress: Consider opening an issue upstream or
# evaluating whether CrowdSec can replace buger/jsonparser with a safe alternative
# GHSA-jqcq-xjh3-6g23: pgproto3/v2 DataRow.Decode panic on negative field length (DoS)
# Severity: HIGH (CVSS 7.5)
# Package: github.com/jackc/pgproto3/v2 v2.3.3 (embedded in /usr/local/bin/crowdsec and /usr/local/bin/cscli)
# Status: NO fix in pgproto3/v2 (archived/EOL) — fix path requires CrowdSec to migrate to pgx/v5
#
# Vulnerability Details:
# - DataRow.Decode does not validate field lengths; a malicious or compromised PostgreSQL server
# can send a negative field length causing a slice-bounds panic — denial of service (CWE-129).
# - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
#
# Root Cause (EOL Module + Third-Party Binary):
# - Charon does not use pgproto3/v2 directly nor communicate with PostgreSQL. The package
# is compiled into CrowdSec binaries for their internal database communication.
# - The pgproto3/v2 module is archived and EOL; no fix will be released. The fix path
# is migration to pgx/v5, which embeds an updated pgproto3/v3.
# - Fix path: once CrowdSec migrates to pgx/v5 and releases an updated binary, rebuild
# the Docker image and remove this suppression.
#
# Risk Assessment: ACCEPTED (Non-exploitable in Charon context + no upstream fix path)
# - The vulnerability requires a malicious PostgreSQL server response. Charon uses SQLite
# internally and does not run PostgreSQL. CrowdSec's database path is not exposed to
# external traffic in a standard Charon deployment.
# - The attack requires a compromised database server, which would imply full host compromise.
#
# Mitigation (active while suppression is in effect):
# - Monitor CrowdSec releases for pgx/v5 migration:
# https://github.com/crowdsecurity/crowdsec/releases
# - Weekly CI security rebuild flags the moment a fixed image ships.
#
# Review:
# - Reviewed 2026-03-19 (initial suppression): pgproto3/v2 is EOL; no fix exists or will exist.
# Waiting on CrowdSec to migrate to pgx/v5. Set 30-day review.
# - Next review: 2026-04-19. Remove suppression once CrowdSec ships with pgx/v5.
#
# Removal Criteria:
# - CrowdSec releases a version with pgx/v5 (pgproto3/v3) replacing pgproto3/v2
# - Rebuild Docker image, run security-scan-docker-image, confirm finding is resolved
# - Remove this entry and the corresponding .trivyignore entry simultaneously
#
# References:
# - GHSA-jqcq-xjh3-6g23: https://github.com/advisories/GHSA-jqcq-xjh3-6g23
# - pgproto3/v2 archive notice: https://github.com/jackc/pgproto3
# - pgx/v5 (replacement): https://github.com/jackc/pgx
# - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
- vulnerability: GHSA-jqcq-xjh3-6g23
package:
name: github.com/jackc/pgproto3/v2
version: "v2.3.3"
type: go-module
reason: |
HIGH — DoS panic via negative field length in pgproto3/v2 v2.3.3 embedded in CrowdSec binaries.
pgproto3/v2 is archived/EOL with no fix planned; fix path requires CrowdSec to migrate to pgx/v5.
Charon uses SQLite, not PostgreSQL; this code path is not reachable in a standard deployment.
Risk accepted; no remediation until CrowdSec ships with pgx/v5.
Reviewed 2026-03-19: pgproto3/v2 EOL confirmed; CrowdSec has not migrated to pgx/v5 yet.
expiry: "2026-04-19" # 30-day review: no fix path until CrowdSec migrates to pgx/v5.
# Action items when this suppression expires:
# 1. Check CrowdSec releases for pgx/v5 migration:
# https://github.com/crowdsecurity/crowdsec/releases
# 2. Verify with: `go version -m /path/to/crowdsec | grep pgproto3`
# Expected: pgproto3/v3 (or no pgproto3 reference if fully replaced)
# 3. If CrowdSec has migrated:
# a. Rebuild Docker image and run local security-scan-docker-image
# b. Remove this suppression entry and the corresponding .trivyignore entry
# 4. If not yet migrated: Extend expiry by 30 days and update the review comment above
# 5. If extended 3+ times: Open an upstream issue on crowdsecurity/crowdsec requesting pgx/v5 migration
# GHSA-x6gf-mpr2-68h6 / CVE-2026-4427: pgproto3/v2 DataRow.Decode panic on negative field length (DoS)
# Severity: HIGH (CVSS 7.5)
# Package: github.com/jackc/pgproto3/v2 v2.3.3 (embedded in /usr/local/bin/crowdsec and /usr/local/bin/cscli)
# Status: NO fix in pgproto3/v2 (archived/EOL) — fix path requires CrowdSec to migrate to pgx/v5
# Note: This is the NVD/Red Hat advisory alias for the same underlying vulnerability as GHSA-jqcq-xjh3-6g23
#
# Vulnerability Details:
# - DataRow.Decode does not validate field lengths; a malicious or compromised PostgreSQL server
# can send a negative field length causing a slice-bounds panic — denial of service (CWE-129).
# - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (CVSS 7.5)
#
# Root Cause (EOL Module + Third-Party Binary):
# - Same underlying vulnerability as GHSA-jqcq-xjh3-6g23; tracked separately by NVD/Red Hat as CVE-2026-4427.
# - Charon does not use pgproto3/v2 directly nor communicate with PostgreSQL. The package
# is compiled into CrowdSec binaries for their internal database communication.
# - The pgproto3/v2 module is archived and EOL; no fix will be released. The fix path
# is migration to pgx/v5, which embeds an updated pgproto3/v3.
# - Fix path: once CrowdSec migrates to pgx/v5 and releases an updated binary, rebuild
# the Docker image and remove this suppression.
#
# Risk Assessment: ACCEPTED (Non-exploitable in Charon context + no upstream fix path)
# - The vulnerability requires a malicious PostgreSQL server response. Charon uses SQLite
# internally and does not run PostgreSQL. CrowdSec's database path is not exposed to
# external traffic in a standard Charon deployment.
# - The attack requires a compromised database server, which would imply full host compromise.
#
# Mitigation (active while suppression is in effect):
# - Monitor CrowdSec releases for pgx/v5 migration:
# https://github.com/crowdsecurity/crowdsec/releases
# - Weekly CI security rebuild flags the moment a fixed image ships.
#
# Review:
# - Reviewed 2026-03-21 (initial suppression): pgproto3/v2 is EOL; no fix exists or will exist.
# Waiting on CrowdSec to migrate to pgx/v5. Set 30-day review. Sibling GHSA-jqcq-xjh3-6g23
# was already suppressed; this alias surfaced as a separate Grype match via NVD/Red Hat tracking.
# - Next review: 2026-04-21. Remove suppression once CrowdSec ships with pgx/v5.
#
# Removal Criteria:
# - Same as GHSA-jqcq-xjh3-6g23: CrowdSec releases a version with pgx/v5 replacing pgproto3/v2
# - Rebuild Docker image, run security-scan-docker-image, confirm both advisories are resolved
# - Remove this entry, GHSA-jqcq-xjh3-6g23 entry, and both .trivyignore entries simultaneously
#
# References:
# - GHSA-x6gf-mpr2-68h6: https://github.com/advisories/GHSA-x6gf-mpr2-68h6
# - CVE-2026-4427: https://nvd.nist.gov/vuln/detail/CVE-2026-4427
# - Red Hat: https://access.redhat.com/security/cve/CVE-2026-4427
# - pgproto3/v2 archive notice: https://github.com/jackc/pgproto3
# - pgx/v5 (replacement): https://github.com/jackc/pgx
# - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
- vulnerability: GHSA-x6gf-mpr2-68h6
package:
name: github.com/jackc/pgproto3/v2
version: "v2.3.3"
type: go-module
reason: |
HIGH — DoS panic via negative field length in pgproto3/v2 v2.3.3 embedded in CrowdSec binaries.
NVD/Red Hat alias (CVE-2026-4427) for the same underlying bug as GHSA-jqcq-xjh3-6g23.
pgproto3/v2 is archived/EOL with no fix planned; fix path requires CrowdSec to migrate to pgx/v5.
Charon uses SQLite, not PostgreSQL; this code path is not reachable in a standard deployment.
Risk accepted; no remediation until CrowdSec ships with pgx/v5.
Reviewed 2026-03-21: pgproto3/v2 EOL confirmed; CrowdSec has not migrated to pgx/v5 yet.
expiry: "2026-04-21" # 30-day review: no fix path until CrowdSec migrates to pgx/v5.
# Action items when this suppression expires:
# 1. Check CrowdSec releases for pgx/v5 migration:
# https://github.com/crowdsecurity/crowdsec/releases
# 2. Verify with: `go version -m /path/to/crowdsec | grep pgproto3`
# Expected: pgproto3/v3 (or no pgproto3 reference if fully replaced)
# 3. If CrowdSec has migrated:
# a. Rebuild Docker image and run local security-scan-docker-image
# b. Remove this entry, GHSA-jqcq-xjh3-6g23 entry, and both .trivyignore entries
# 4. If not yet migrated: Extend expiry by 30 days and update the review comment above
# 5. If extended 3+ times: Open an upstream issue on crowdsecurity/crowdsec requesting pgx/v5 migration
# Match exclusions (patterns to ignore during scanning)
# Use sparingly - prefer specific CVE suppressions above
match:

View File

@@ -1,211 +0,0 @@
# NOTE: golangci-lint-fast now includes test files (_test.go) to catch security
# issues earlier. The fast config uses gosec with critical-only checks (G101,
# G110, G305, G401, G501, G502, G503) for acceptable performance.
# Last updated: 2026-02-02
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: end-of-file-fixer
exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|.*\.tsbuildinfo$)'
- id: trailing-whitespace
exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|.*\.tsbuildinfo$)'
- id: check-yaml
- id: check-added-large-files
args: ['--maxkb=2500']
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck
name: shellcheck
exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|test-results|codeql-agent-results)/'
args: ['--severity=error']
- repo: https://github.com/rhysd/actionlint
rev: v1.7.10
hooks:
- id: actionlint
name: actionlint (GitHub Actions)
files: '^\.github/workflows/.*\.ya?ml$'
- repo: local
hooks:
- id: dockerfile-check
name: dockerfile validation
entry: tools/dockerfile_check.sh
language: script
files: "Dockerfile.*"
pass_filenames: true
- id: go-test-coverage
name: Go Test Coverage (Manual)
entry: scripts/go-test-coverage.sh
language: script
files: '\.go$'
pass_filenames: false
verbose: true
stages: [manual] # Only runs when explicitly called
- id: go-vet
name: Go Vet
entry: bash -c 'cd backend && go vet ./...'
language: system
files: '\.go$'
pass_filenames: false
- id: golangci-lint-fast
name: golangci-lint (Fast Linters - BLOCKING)
entry: scripts/pre-commit-hooks/golangci-lint-fast.sh
language: script
files: '\.go$'
# Test files are now included to catch security issues (gosec critical checks)
pass_filenames: false
description: "Runs fast, essential linters (staticcheck, govet, errcheck, ineffassign, unused, gosec critical) - BLOCKS commits on failure"
- id: check-version-match
name: Check .version matches latest Git tag
entry: bash -c 'scripts/check-version-match-tag.sh'
language: system
files: '\.version$'
pass_filenames: false
- id: check-lfs-large-files
name: Prevent large files that are not tracked by LFS
entry: bash scripts/pre-commit-hooks/check-lfs-for-large-files.sh
language: system
pass_filenames: false
verbose: true
always_run: true
- id: block-codeql-db-commits
name: Prevent committing CodeQL DB artifacts
entry: bash scripts/pre-commit-hooks/block-codeql-db-commits.sh
language: system
pass_filenames: false
verbose: true
always_run: true
- id: block-data-backups-commit
name: Prevent committing data/backups files
entry: bash scripts/pre-commit-hooks/block-data-backups-commit.sh
language: system
pass_filenames: false
verbose: true
always_run: true
# === MANUAL/CI-ONLY HOOKS ===
# These are slow and should only run on-demand or in CI
# Run manually with: pre-commit run golangci-lint-full --all-files
- id: go-test-race
name: Go Test Race (Manual)
entry: bash -c 'cd backend && go test -race ./...'
language: system
files: '\.go$'
pass_filenames: false
stages: [manual] # Only runs when explicitly called
- id: golangci-lint-full
name: golangci-lint (Full - Manual)
entry: scripts/pre-commit-hooks/golangci-lint-full.sh
language: script
files: '\.go$'
pass_filenames: false
stages: [manual] # Only runs when explicitly called
- id: hadolint
name: Hadolint Dockerfile Check (Manual)
entry: bash -c 'docker run --rm -i hadolint/hadolint < Dockerfile'
language: system
files: 'Dockerfile'
pass_filenames: false
stages: [manual] # Only runs when explicitly called
- id: frontend-type-check
name: Frontend TypeScript Check
entry: bash -c 'cd frontend && npx tsc --noEmit'
language: system
files: '^frontend/.*\.(ts|tsx)$'
pass_filenames: false
- id: frontend-lint
name: Frontend Lint (Fix)
entry: bash -c 'cd frontend && npm run lint -- --fix'
language: system
files: '^frontend/.*\.(ts|tsx|js|jsx)$'
pass_filenames: false
- id: frontend-test-coverage
name: Frontend Test Coverage (Manual)
entry: scripts/frontend-test-coverage.sh
language: script
files: '^frontend/.*\\.(ts|tsx|js|jsx)$'
pass_filenames: false
verbose: true
stages: [manual]
- id: security-scan
name: Security Vulnerability Scan (Manual)
entry: scripts/security-scan.sh
language: script
files: '(\.go$|go\.mod$|go\.sum$)'
pass_filenames: false
verbose: true
stages: [manual] # Only runs when explicitly called
- id: codeql-go-scan
name: CodeQL Go Security Scan (Manual - Slow)
entry: scripts/pre-commit-hooks/codeql-go-scan.sh
language: script
files: '\.go$'
pass_filenames: false
verbose: true
stages: [manual] # Performance: 30-60s, only run on-demand
- id: codeql-js-scan
name: CodeQL JavaScript/TypeScript Security Scan (Manual - Slow)
entry: scripts/pre-commit-hooks/codeql-js-scan.sh
language: script
files: '^frontend/.*\.(ts|tsx|js|jsx)$'
pass_filenames: false
verbose: true
stages: [manual] # Performance: 30-60s, only run on-demand
- id: codeql-check-findings
name: Block HIGH/CRITICAL CodeQL Findings
entry: scripts/pre-commit-hooks/codeql-check-findings.sh
language: script
pass_filenames: false
verbose: true
stages: [manual] # Only runs after CodeQL scans
- id: codeql-parity-check
name: CodeQL Suite/Trigger Parity Guard (Manual)
entry: scripts/ci/check-codeql-parity.sh
language: script
pass_filenames: false
verbose: true
stages: [manual]
- id: gorm-security-scan
name: GORM Security Scanner (Manual)
entry: scripts/pre-commit-hooks/gorm-security-check.sh
language: script
files: '\.go$'
pass_filenames: false
stages: [manual] # Manual stage initially (soft launch)
verbose: true
description: "Detects GORM ID leaks and common GORM security mistakes"
- id: semgrep-scan
name: Semgrep Security Scan (Manual)
entry: scripts/pre-commit-hooks/semgrep-scan.sh
language: script
pass_filenames: false
verbose: true
stages: [manual] # Manual stage initially (reversible rollout)
- id: gitleaks-tuned-scan
name: Gitleaks Security Scan (Tuned, Manual)
entry: scripts/pre-commit-hooks/gitleaks-tuned-scan.sh
language: script
pass_filenames: false
verbose: true
stages: [manual] # Manual stage initially (reversible rollout)
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.47.0
hooks:
- id: markdownlint
args: ["--fix"]
exclude: '^(node_modules|\.venv|test-results|codeql-db|codeql-agent-results)/'
stages: [manual]

View File

@@ -7,3 +7,74 @@ playwright/.auth/
# Charon does not use Nebula VPN PKI by default. Review by: 2026-03-05
# See also: .grype.yaml for full justification
CVE-2026-25793
# CVE-2026-22184: zlib Global Buffer Overflow in untgz utility
# Severity: CRITICAL (CVSS 9.8) — Package: zlib 1.3.1-r2 in Alpine base image
# No upstream fix available: Alpine 3.23 (including edge) still ships zlib 1.3.1-r2.
# Charon does not use untgz or process untrusted tar archives. Review by: 2026-03-14
# See also: .grype.yaml for full justification
CVE-2026-22184
# CVE-2026-27171: zlib CPU spin via crc32_combine64 infinite loop (DoS)
# Severity: MEDIUM (CVSS 5.5 NVD / 2.9 MITRE) — Package: zlib 1.3.1-r2 in Alpine base image
# Fix requires zlib >= 1.3.2. No upstream fix available: Alpine 3.23 still ships zlib 1.3.1-r2.
# Attack requires local access (AV:L); the vulnerable code path is not reachable via Charon's
# network-facing surface. Non-blocking by CI policy (MEDIUM). Review by: 2026-04-21
# exp: 2026-04-21
CVE-2026-27171
# CVE-2026-2673: OpenSSL TLS 1.3 server key exchange group downgrade (libcrypto3/libssl3)
# Severity: HIGH (CVSS 7.5) — Packages: libcrypto3 3.5.5-r0 and libssl3 3.5.5-r0 in Alpine base image
# No upstream fix available: Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-03-18.
# When DEFAULT is in TLS 1.3 group config, server may select a weaker key exchange group.
# Charon terminates TLS at the Caddy layer — the Go backend does not act as a raw TLS 1.3 server.
# Review by: 2026-04-18
# See also: .grype.yaml for full justification
# exp: 2026-04-18
CVE-2026-2673
# CVE-2026-33186 / GHSA-p77j-4mvh-x3m3: gRPC-Go authorization bypass via missing leading slash
# Severity: CRITICAL (CVSS 9.1) — Package: google.golang.org/grpc, embedded in CrowdSec (v1.74.2) and Caddy (v1.79.1)
# Fix exists at v1.79.3 — Charon's own dep is patched. Waiting on CrowdSec and Caddy upstream releases.
# CrowdSec's and Caddy's grpc servers are not exposed externally in a standard Charon deployment.
# Review by: 2026-04-02
# See also: .grype.yaml for full justification
# exp: 2026-04-02
CVE-2026-33186
# GHSA-479m-364c-43vc: goxmldsig XML signature validation bypass (loop variable capture)
# Severity: HIGH (CVSS 7.5) — Package: github.com/russellhaering/goxmldsig v1.5.0, embedded in /usr/bin/caddy
# Fix exists at v1.6.0 — waiting on Caddy upstream (or caddy-security plugin) to release with patched goxmldsig.
# Charon does not configure SAML-based SSO by default; the vulnerable path is not reachable in a standard deployment.
# Review by: 2026-04-02
# See also: .grype.yaml for full justification
# exp: 2026-04-02
GHSA-479m-364c-43vc
# GHSA-6g7g-w4f8-9c9x: buger/jsonparser Delete panic on malformed JSON (DoS)
# Severity: HIGH (CVSS 7.5) — Package: github.com/buger/jsonparser v1.1.1, embedded in CrowdSec binaries
# No upstream fix available as of 2026-03-19 (issue #275 open, golang/vulndb #4514 open).
# Charon does not use this package; the vector requires reaching CrowdSec's internal processing pipeline.
# Review by: 2026-04-19
# See also: .grype.yaml for full justification
# exp: 2026-04-19
GHSA-6g7g-w4f8-9c9x
# GHSA-jqcq-xjh3-6g23: pgproto3/v2 DataRow.Decode panic on negative field length (DoS)
# Severity: HIGH (CVSS 7.5) — Package: github.com/jackc/pgproto3/v2 v2.3.3, embedded in CrowdSec binaries
# pgproto3/v2 is archived/EOL — no fix will be released. Fix path requires CrowdSec to migrate to pgx/v5.
# Charon uses SQLite; the PostgreSQL code path is not reachable in a standard deployment.
# Review by: 2026-04-19
# See also: .grype.yaml for full justification
# exp: 2026-04-19
GHSA-jqcq-xjh3-6g23
# GHSA-x6gf-mpr2-68h6 / CVE-2026-4427: pgproto3/v2 DataRow.Decode panic on negative field length (DoS)
# Severity: HIGH (CVSS 7.5) — Package: github.com/jackc/pgproto3/v2 v2.3.3, embedded in CrowdSec binaries
# NVD/Red Hat alias (CVE-2026-4427) for the same underlying bug as GHSA-jqcq-xjh3-6g23.
# pgproto3/v2 is archived/EOL — no fix will be released. Fix path requires CrowdSec to migrate to pgx/v5.
# Charon uses SQLite; the PostgreSQL code path is not reachable in a standard deployment.
# Review by: 2026-04-21
# See also: .grype.yaml for full justification
# exp: 2026-04-21
GHSA-x6gf-mpr2-68h6

12
.vscode/tasks.json vendored
View File

@@ -371,9 +371,9 @@
}
},
{
"label": "Lint: Pre-commit (All Files)",
"label": "Lint: Lefthook Pre-commit (All Files)",
"type": "shell",
"command": ".github/skills/scripts/skill-runner.sh qa-precommit-all",
"command": "lefthook run pre-commit",
"group": "test",
"problemMatcher": []
},
@@ -466,9 +466,9 @@
"problemMatcher": []
},
{
"label": "Security: Semgrep Scan (Manual Hook)",
"label": "Security: Semgrep Scan (Lefthook Pre-push)",
"type": "shell",
"command": "pre-commit run --hook-stage manual semgrep-scan --all-files",
"command": "lefthook run pre-push",
"group": "test",
"problemMatcher": []
},
@@ -480,9 +480,9 @@
"problemMatcher": []
},
{
"label": "Security: Gitleaks Scan (Tuned Manual Hook)",
"label": "Security: Gitleaks Scan (Lefthook Pre-push)",
"type": "shell",
"command": "pre-commit run --hook-stage manual gitleaks-tuned-scan --all-files",
"command": "lefthook run pre-push",
"group": "test",
"problemMatcher": []
},

View File

@@ -139,15 +139,15 @@ graph TB
| Component | Technology | Version | Purpose |
|-----------|-----------|---------|---------|
| **Framework** | React | 19.2.3 | UI framework |
| **Language** | TypeScript | 5.x | Type-safe JavaScript |
| **Build Tool** | Vite | 6.1.9 | Fast bundler and dev server |
| **CSS Framework** | Tailwind CSS | 3.x | Utility-first CSS |
| **Language** | TypeScript | 6.x | Type-safe JavaScript |
| **Build Tool** | Vite | 8.0.0-beta.18 | Fast bundler and dev server |
| **CSS Framework** | Tailwind CSS | 4.2.1 | Utility-first CSS |
| **Routing** | React Router | 7.x | Client-side routing |
| **HTTP Client** | Fetch API | Native | API communication |
| **State Management** | React Hooks + Context | Native | Global state |
| **Internationalization** | i18next | Latest | 5 language support |
| **Unit Testing** | Vitest | 2.x | Fast unit test runner |
| **E2E Testing** | Playwright | 1.50.x | Browser automation |
| **Unit Testing** | Vitest | 4.1.0-beta.6 | Fast unit test runner |
| **E2E Testing** | Playwright | 1.58.2 | Browser automation |
### Infrastructure
@@ -218,7 +218,7 @@ graph TB
│ │ └── main.tsx # Application entry point
│ ├── public/ # Static assets
│ ├── package.json # NPM dependencies
│ └── vite.config.js # Vite configuration
│ └── vite.config.ts # Vite configuration
├── .docker/ # Docker configuration
│ ├── compose/ # Docker Compose files

View File

@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **Pushover Notification Provider**: Send push notifications to your devices via the Pushover app
- Supports JSON templates (minimal, detailed, custom)
- Application API Token stored securely — never exposed in API responses
- User Key stored in the URL field, following the same pattern as Telegram
- Feature flag: `feature.notifications.service.pushover.enabled` (on by default)
- Emergency priority (2) is intentionally unsupported — deferred to a future release
- **Slack Notification Provider**: Send alerts to Slack channels via Incoming Webhooks
- Supports JSON templates (minimal, detailed, custom) with Slack's native `text` format
- Webhook URL stored securely — never exposed in API responses
- Optional channel display name for easy identification in provider list
- Feature flag: `feature.notifications.service.slack.enabled` (on by default)
- See [Notification Guide](docs/features/notifications.md) for setup instructions
### CI/CD
- **Supply Chain**: Optimized verification workflow to prevent redundant builds
- Change: Removed direct Push/PR triggers; now waits for 'Docker Build' via `workflow_run`
@@ -29,6 +45,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Prevents timeout errors in Firefox/WebKit caused by strict label matching
### Fixed
- **TCP Monitor Creation**: Fixed misleading form UX that caused silent HTTP 500 errors when creating TCP monitors
- Corrected URL placeholder to show `host:port` format instead of the incorrect `tcp://host:port` prefix
- Added dynamic per-type placeholder and helper text (HTTP monitors show a full URL example; TCP monitors show `host:port`)
- Added client-side validation that blocks form submission when a scheme prefix (e.g. `tcp://`) is detected, with an inline error message
- Reordered form fields so the monitor type selector appears above the URL input, making the dynamic helper text immediately relevant
- i18n: Added 5 new translation keys across en, de, fr, es, and zh locales
- **CI: Rate Limit Integration Tests**: Hardened test script reliability — login now validates HTTP status, Caddy admin API readiness gated on `/config/` poll, security config failures are fatal with full diagnostics, and poll interval increased to 5s
- **CI: Rate Limit Integration Tests**: Removed stale GeoIP database SHA256 checksum from Dockerfile non-CI path (hash was perpetually stale due to weekly upstream updates)
- **CI: Rate Limit Integration Tests**: Fixed Caddy admin API debug dump URL to use canonical trailing slash in workflow
- Fixed: Added robust validation and debug logging for Docker image tags to prevent invalid reference errors.
- Fixed: Removed log masking for image references and added manifest validation to debug CI failures.
- **Proxy Hosts**: Fixed ACL and Security Headers dropdown selections so create/edit saves now keep the selected values (including clearing to none) after submit and reload.

View File

@@ -33,7 +33,19 @@ This project follows a Code of Conduct that all contributors are expected to adh
### Development Tools
Install golangci-lint for pre-commit hooks (required for Go development):
Install golangci-lint for lefthook pre-commit-phase hooks (required for Go development):
Also install lefthook itself so the git hooks work:
```bash
# Option 1: Homebrew (macOS/Linux)
brew install lefthook
# Option 2: Go install
go install github.com/evilmartians/lefthook@latest
```
```bash
# Option 1: Homebrew (macOS/Linux)
@@ -59,7 +71,7 @@ golangci-lint --version
# Should output: golangci-lint has version 1.xx.x ...
```
**Note:** Pre-commit hooks will **BLOCK commits** if golangci-lint finds issues. This is intentional - fix the issues before committing.
**Note:** Lefthook pre-commit-phase hooks will **BLOCK commits** if golangci-lint finds issues. This is intentional - fix the issues before committing.
### CI/CD Go Version Management
@@ -84,7 +96,7 @@ When the project's Go version is updated (usually by Renovate):
3. **Rebuild your development tools**
```bash
# This fixes pre-commit hook errors and IDE issues
# This fixes lefthook hook errors and IDE issues
./scripts/rebuild-go-tools.sh
```
@@ -104,7 +116,7 @@ Rebuilding tools with `./scripts/rebuild-go-tools.sh` fixes this by compiling th
**What if I forget?**
Don't worry! The pre-commit hook will detect the version mismatch and automatically rebuild tools for you. You'll see:
Don't worry! The lefthook pre-commit hook will detect the version mismatch and automatically rebuild tools for you. You'll see:
```
⚠️ golangci-lint Go version mismatch:

View File

@@ -8,6 +8,29 @@ ARG VCS_REF
# Set BUILD_DEBUG=1 to build with debug symbols (required for Delve debugging)
ARG BUILD_DEBUG=0
# ---- Pinned Toolchain Versions ----
# renovate: datasource=docker depName=golang versioning=docker
ARG GO_VERSION=1.26.1
# renovate: datasource=docker depName=alpine versioning=docker
ARG ALPINE_IMAGE=alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
# ---- Shared CrowdSec Version ----
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
ARG CROWDSEC_VERSION=1.7.6
# CrowdSec fallback tarball checksum (v${CROWDSEC_VERSION})
ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0bfe5e38f863bd
# ---- Shared Go Security Patches ----
# renovate: datasource=go depName=github.com/expr-lang/expr
ARG EXPR_LANG_VERSION=1.17.8
# renovate: datasource=go depName=golang.org/x/net
ARG XNET_VERSION=0.52.0
# renovate: datasource=go depName=github.com/smallstep/certificates
ARG SMALLSTEP_CERTIFICATES_VERSION=0.30.0
# renovate: datasource=npm depName=npm
ARG NPM_VERSION=11.11.1
# Allow pinning Caddy version - Renovate will update this
# Build the most recent Caddy 2.x release (keeps major pinned under v3).
# Setting this to '2' tells xcaddy to resolve the latest v2.x tag so we
@@ -20,14 +43,14 @@ ARG CADDY_CANDIDATE_VERSION=2.11.2
ARG CADDY_USE_CANDIDATE=0
ARG CADDY_PATCH_SCENARIO=B
# renovate: datasource=go depName=github.com/greenpau/caddy-security
ARG CADDY_SECURITY_VERSION=1.1.36
ARG CADDY_SECURITY_VERSION=1.1.50
# renovate: datasource=go depName=github.com/corazawaf/coraza-caddy
ARG CORAZA_CADDY_VERSION=2.2.0
## When an official caddy image tag isn't available on the host, use a
## plain Alpine base image and overwrite its caddy binary with our
## xcaddy-built binary in the later COPY step. This avoids relying on
## upstream caddy image tags while still shipping a pinned caddy binary.
## Alpine 3.23 base to reduce glibc CVE exposure and image size.
# renovate: datasource=docker depName=alpine versioning=docker
ARG CADDY_IMAGE=alpine:3.23.3
# ---- Cross-Compilation Helpers ----
# renovate: datasource=docker depName=tonistiigi/xx
@@ -38,8 +61,7 @@ FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f9
# This fixes 22 HIGH/CRITICAL CVEs in stdlib embedded in Debian's gosu package
# CVEs fixed: CVE-2023-24531, CVE-2023-24540, CVE-2023-29402, CVE-2023-29404,
# CVE-2023-29405, CVE-2024-24790, CVE-2025-22871, and 15 more
# renovate: datasource=docker depName=golang
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS gosu-builder
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS gosu-builder
COPY --from=xx / /
WORKDIR /tmp/gosu
@@ -70,7 +92,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# ---- Frontend Builder ----
# Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues
# renovate: datasource=docker depName=node
FROM --platform=$BUILDPLATFORM node:24.14.0-alpine AS frontend-builder
FROM --platform=$BUILDPLATFORM node:24.14.0-alpine@sha256:7fddd9ddeae8196abf4a3ef2de34e11f7b1a722119f91f28ddf1e99dcafdf114 AS frontend-builder
WORKDIR /app/frontend
# Copy frontend package files
@@ -81,9 +103,12 @@ ARG VERSION=dev
# Make version available to Vite as VITE_APP_VERSION during the frontend build
ENV VITE_APP_VERSION=${VERSION}
# Set environment to bypass native binary requirement for cross-arch builds
ENV npm_config_rollup_skip_nodejs_native=1 \
ROLLUP_SKIP_NODEJS_NATIVE=1
# Vite 8: Rolldown native bindings auto-resolved per platform via optionalDependencies
ARG NPM_VERSION
# hadolint ignore=DL3017
RUN apk upgrade --no-cache && \
npm install -g npm@${NPM_VERSION} --no-fund --no-audit && \
npm cache clean --force
RUN npm ci
@@ -93,8 +118,7 @@ RUN --mount=type=cache,target=/app/frontend/node_modules/.cache \
npm run build
# ---- Backend Builder ----
# renovate: datasource=docker depName=golang
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS backend-builder
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS backend-builder
# Copy xx helpers for cross-compilation
COPY --from=xx / /
@@ -196,8 +220,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# ---- Caddy Builder ----
# Build Caddy from source to ensure we use the latest Go version and dependencies
# This fixes vulnerabilities found in the pre-built Caddy images (e.g. CVE-2025-59530, stdlib issues)
# renovate: datasource=docker depName=golang
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS caddy-builder
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS caddy-builder
ARG TARGETOS
ARG TARGETARCH
ARG CADDY_VERSION
@@ -205,11 +228,15 @@ ARG CADDY_CANDIDATE_VERSION
ARG CADDY_USE_CANDIDATE
ARG CADDY_PATCH_SCENARIO
ARG CADDY_SECURITY_VERSION
ARG CORAZA_CADDY_VERSION
# renovate: datasource=go depName=github.com/caddyserver/xcaddy
ARG XCADDY_VERSION=0.4.5
ARG EXPR_LANG_VERSION
ARG XNET_VERSION
ARG SMALLSTEP_CERTIFICATES_VERSION
# hadolint ignore=DL3018
RUN apk add --no-cache git
RUN apk add --no-cache bash git
# hadolint ignore=DL3062
RUN --mount=type=cache,target=/go/pkg/mod \
go install github.com/caddyserver/xcaddy/cmd/xcaddy@v${XCADDY_VERSION}
@@ -221,7 +248,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
# hadolint ignore=SC2016
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
sh -c 'set -e; \
bash -c 'set -e; \
CADDY_TARGET_VERSION="${CADDY_VERSION}"; \
if [ "${CADDY_USE_CANDIDATE}" = "1" ]; then \
CADDY_TARGET_VERSION="${CADDY_CANDIDATE_VERSION}"; \
@@ -234,7 +261,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_TARGET_VERSION} \
--with github.com/caddyserver/caddy/v2@v${CADDY_TARGET_VERSION} \
--with github.com/greenpau/caddy-security@v${CADDY_SECURITY_VERSION} \
--with github.com/corazawaf/coraza-caddy/v2 \
--with github.com/corazawaf/coraza-caddy/v2@v${CORAZA_CADDY_VERSION} \
--with github.com/hslatman/caddy-crowdsec-bouncer@v0.10.0 \
--with github.com/zhangjiayin/caddy-geoip2 \
--with github.com/mholt/caddy-ratelimit \
@@ -251,10 +278,24 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
# Patch ALL dependencies BEFORE building the final binary
# These patches fix CVEs in transitive dependencies
# Renovate tracks these via regex manager in renovate.json
# renovate: datasource=go depName=github.com/expr-lang/expr
go get github.com/expr-lang/expr@v1.17.7; \
go get github.com/expr-lang/expr@v${EXPR_LANG_VERSION}; \
# renovate: datasource=go depName=github.com/hslatman/ipstore
go get github.com/hslatman/ipstore@v0.4.0; \
go get golang.org/x/net@v${XNET_VERSION}; \
# CVE-2026-33186 (GHSA-p77j-4mvh-x3m3): gRPC-Go auth bypass via missing leading slash
# Fix available at v1.79.3. Pin here so the Caddy binary is patched immediately;
# remove once Caddy ships a release built with grpc >= v1.79.3.
# renovate: datasource=go depName=google.golang.org/grpc
go get google.golang.org/grpc@v1.79.3; \
# GHSA-479m-364c-43vc: goxmldsig XML signature validation bypass (loop variable capture)
# Fix available at v1.6.0. Pin here so the Caddy binary is patched immediately;
# remove once caddy-security ships a release built with goxmldsig >= v1.6.0.
# renovate: datasource=go depName=github.com/russellhaering/goxmldsig
go get github.com/russellhaering/goxmldsig@v1.6.0; \
# CVE-2026-30836: smallstep/certificates 0.30.0-rc3 vulnerability
# Fix available at v0.30.0. Pin here so the Caddy binary is patched immediately;
# remove once caddy-security ships a release built with smallstep/certificates >= v0.30.0.
go get github.com/smallstep/certificates@v${SMALLSTEP_CERTIFICATES_VERSION}; \
if [ "${CADDY_PATCH_SCENARIO}" = "A" ]; then \
# Rollback scenario: keep explicit nebula pin if upstream compatibility regresses.
# NOTE: smallstep/certificates (pulled by caddy-security stack) currently
@@ -288,10 +329,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
rm -rf /tmp/buildenv_* /tmp/caddy-initial'
# ---- CrowdSec Builder ----
# Build CrowdSec from source to ensure we use Go 1.26.0+ and avoid stdlib vulnerabilities
# Build CrowdSec from source to ensure we use Go 1.26.1+ and avoid stdlib vulnerabilities
# (CVE-2025-58183, CVE-2025-58186, CVE-2025-58187, CVE-2025-61729)
# renovate: datasource=docker depName=golang versioning=docker
FROM --platform=$BUILDPLATFORM golang:1.26.0-alpine AS crowdsec-builder
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS crowdsec-builder
COPY --from=xx / /
WORKDIR /tmp/crowdsec
@@ -299,11 +339,10 @@ WORKDIR /tmp/crowdsec
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
# CrowdSec version - Renovate can update this
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
ARG CROWDSEC_VERSION=1.7.6
# CrowdSec fallback tarball checksum (v${CROWDSEC_VERSION})
ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0bfe5e38f863bd
ARG CROWDSEC_VERSION
ARG CROWDSEC_RELEASE_SHA256
ARG EXPR_LANG_VERSION
ARG XNET_VERSION
# hadolint ignore=DL3018
RUN apk add --no-cache git clang lld
@@ -317,10 +356,15 @@ RUN git clone --depth 1 --branch "v${CROWDSEC_VERSION}" https://github.com/crowd
# Patch dependencies to fix CVEs in transitive dependencies
# This follows the same pattern as Caddy's dependency patches
# renovate: datasource=go depName=github.com/expr-lang/expr
# renovate: datasource=go depName=golang.org/x/crypto
RUN go get github.com/expr-lang/expr@v1.17.7 && \
RUN go get github.com/expr-lang/expr@v${EXPR_LANG_VERSION} && \
go get golang.org/x/crypto@v0.46.0 && \
go get golang.org/x/net@v${XNET_VERSION} && \
# CVE-2026-33186 (GHSA-p77j-4mvh-x3m3): gRPC-Go auth bypass via missing leading slash
# Fix available at v1.79.3. Pin here so the CrowdSec binary is patched immediately;
# remove once CrowdSec ships a release built with grpc >= v1.79.3.
# renovate: datasource=go depName=google.golang.org/grpc
go get google.golang.org/grpc@v1.79.3 && \
go mod tidy
# Fix compatibility issues with expr-lang v1.17.7
@@ -350,18 +394,15 @@ RUN mkdir -p /crowdsec-out/config && \
cp -r config/* /crowdsec-out/config/ || true
# ---- CrowdSec Fallback (for architectures where build fails) ----
# renovate: datasource=docker depName=alpine versioning=docker
FROM alpine:3.23.3 AS crowdsec-fallback
FROM ${ALPINE_IMAGE} AS crowdsec-fallback
SHELL ["/bin/ash", "-o", "pipefail", "-c"]
WORKDIR /tmp/crowdsec
ARG TARGETARCH
# CrowdSec version - Renovate can update this
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
ARG CROWDSEC_VERSION=1.7.6
ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0bfe5e38f863bd
ARG CROWDSEC_VERSION
ARG CROWDSEC_RELEASE_SHA256
# hadolint ignore=DL3018
RUN apk add --no-cache curl ca-certificates
@@ -390,17 +431,17 @@ RUN set -eux; \
fi
# ---- Final Runtime with Caddy ----
FROM ${CADDY_IMAGE}
FROM ${ALPINE_IMAGE}
WORKDIR /app
# Install runtime dependencies for Charon, including bash for maintenance scripts
# Note: gosu is now built from source (see gosu-builder stage) to avoid CVEs from Debian's pre-compiled version
# Explicitly upgrade packages to fix security vulnerabilities
# binutils provides objdump for debug symbol detection in docker-entrypoint.sh
# hadolint ignore=DL3018
RUN apk add --no-cache \
bash ca-certificates sqlite-libs sqlite tzdata curl gettext libcap libcap-utils \
c-ares binutils libc-utils busybox-extras
bash ca-certificates sqlite-libs sqlite tzdata gettext libcap libcap-utils \
c-ares busybox-extras \
&& apk upgrade --no-cache zlib
# Copy gosu binary from gosu-builder (built with Go 1.26+ to avoid stdlib CVEs)
COPY --from=gosu-builder /gosu-out/gosu /usr/sbin/gosu
@@ -417,12 +458,13 @@ SHELL ["/bin/ash", "-o", "pipefail", "-c"]
# Note: In production, users should provide their own MaxMind license key
# This uses the publicly available GeoLite2 database
# In CI, timeout quickly rather than retrying to save build time
ARG GEOLITE2_COUNTRY_SHA256=b79afc28a0a52f89c15e8d92b05c173f314dd4f687719f96cf921012d900fcce
ARG GEOLITE2_COUNTRY_SHA256=aa154fc6bcd712644de232a4abcdd07dac1f801308c0b6f93dbc2b375443da7b
RUN mkdir -p /app/data/geoip && \
if [ -n "$CI" ]; then \
if [ "$CI" = "true" ] || [ "$CI" = "1" ]; then \
echo "⏱️ CI detected - quick download (10s timeout, no retries)"; \
if curl -fSL -m 10 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
-o /app/data/geoip/GeoLite2-Country.mmdb 2>/dev/null; then \
if wget -qO /app/data/geoip/GeoLite2-Country.mmdb \
-T 10 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" 2>/dev/null \
&& [ -s /app/data/geoip/GeoLite2-Country.mmdb ]; then \
echo "✅ GeoIP downloaded"; \
else \
echo "⚠️ GeoIP skipped"; \
@@ -430,16 +472,12 @@ RUN mkdir -p /app/data/geoip && \
fi; \
else \
echo "Local - full download (30s timeout, 3 retries)"; \
if curl -fSL -m 30 --retry 3 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
-o /app/data/geoip/GeoLite2-Country.mmdb; then \
if echo "${GEOLITE2_COUNTRY_SHA256} /app/data/geoip/GeoLite2-Country.mmdb" | sha256sum -c -; then \
echo "✅ GeoIP checksum verified"; \
else \
echo "⚠️ Checksum failed"; \
touch /app/data/geoip/GeoLite2-Country.mmdb.placeholder; \
fi; \
if wget -qO /app/data/geoip/GeoLite2-Country.mmdb \
-T 30 -t 4 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
&& [ -s /app/data/geoip/GeoLite2-Country.mmdb ]; then \
echo "✅ GeoIP downloaded"; \
else \
echo "⚠️ Download failed"; \
echo "⚠️ GeoIP download failed or empty — skipping"; \
touch /app/data/geoip/GeoLite2-Country.mmdb.placeholder; \
fi; \
fi
@@ -450,7 +488,7 @@ COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
# Allow non-root to bind privileged ports (80/443) securely
RUN setcap 'cap_net_bind_service=+ep' /usr/bin/caddy
# Copy CrowdSec binaries from the crowdsec-builder stage (built with Go 1.26.0+)
# Copy CrowdSec binaries from the crowdsec-builder stage (built with Go 1.26.1+)
# This ensures we don't have stdlib vulnerabilities from older Go versions
COPY --from=crowdsec-builder /crowdsec-out/crowdsec /usr/local/bin/crowdsec
COPY --from=crowdsec-builder /crowdsec-out/cscli /usr/local/bin/cscli
@@ -565,8 +603,8 @@ EXPOSE 80 443 443/udp 2019 8080
# Security: Add healthcheck to monitor container health
# Verifies the Charon API is responding correctly
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/api/v1/health || exit 1
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
CMD wget -q -O /dev/null http://localhost:8080/api/v1/health || exit 1
# Create CrowdSec symlink as root before switching to non-root user
# This symlink allows CrowdSec to use persistent storage at /app/data/crowdsec/config

View File

@@ -1,4 +1,4 @@
.PHONY: help install test build run clean docker-build docker-run release go-check gopls-logs lint-fast lint-staticcheck-only
.PHONY: help install test build run clean docker-build docker-run release go-check gopls-logs lint-fast lint-staticcheck-only security-local
# Default target
help:
@@ -22,6 +22,7 @@ help:
@echo ""
@echo "Security targets:"
@echo " security-scan - Quick security scan (govulncheck on Go deps)"
@echo " security-local - Run govulncheck + semgrep (p/golang) locally before push"
@echo " security-scan-full - Full container scan with Trivy"
@echo " security-scan-deps - Check for outdated Go dependencies"
@@ -145,6 +146,12 @@ security-scan:
@echo "Running security scan (govulncheck)..."
@./scripts/security-scan.sh
security-local: ## Run govulncheck + semgrep (p/golang) before push — fast local gate
@echo "[1/2] Running govulncheck..."
@./scripts/security-scan.sh
@echo "[2/2] Running Semgrep (p/golang, ERROR+WARNING)..."
@SEMGREP_CONFIG=p/golang ./scripts/pre-commit-hooks/semgrep-scan.sh
security-scan-full:
@echo "Building local Docker image for security scan..."
docker build --build-arg VCS_REF=$(shell git rev-parse HEAD) -t charon:local .

View File

@@ -11,60 +11,278 @@ We release security updates for the following versions:
## Reporting a Vulnerability
We take security seriously. If you discover a security vulnerability in Charon, please report it responsibly.
To report a security issue, use
[GitHub Private Security Advisories](https://github.com/Wikid82/charon/security/advisories/new)
or open a [GitHub Issue](https://github.com/Wikid82/Charon/issues) for non-sensitive disclosures.
### Where to Report
Please include a description, reproduction steps, impact assessment, and a non-destructive proof of
concept where possible.
**Preferred Method**: GitHub Security Advisory (Private)
We will acknowledge your report within **48 hours** and provide a remediation timeline within
**7 days**. Reporters are credited in release notes with their consent. We do not pursue legal
action against good-faith security researchers. Please allow **90 days** from initial report before
public disclosure.
1. Go to <https://github.com/Wikid82/charon/security/advisories/new>
2. Fill out the advisory form with:
- Vulnerability description
- Steps to reproduce
- Proof of concept (non-destructive)
- Impact assessment
- Suggested fix (if applicable)
---
**Alternative Method**: GitHub Issues (Public)
## Known Vulnerabilities
1. Go to <https://github.com/Wikid82/Charon/issues>
2. Create a new issue with the same information as above
### [CRITICAL] CVE-2025-68121 · Go Stdlib Critical in CrowdSec Bundled Binaries
### What to Include
| Field | Value |
|--------------|-------|
| **ID** | CVE-2025-68121 (see also CHARON-2025-001) |
| **Severity** | Critical |
| **Status** | Awaiting Upstream |
Please provide:
**What**
A critical Go standard library vulnerability affects CrowdSec binaries bundled in the Charon
container image. The binaries were compiled against Go 1.25.6, which contains this flaw.
Charon's own application code, compiled with Go 1.26.1, is unaffected.
1. **Description**: Clear explanation of the vulnerability
2. **Reproduction Steps**: Detailed steps to reproduce the issue
3. **Impact Assessment**: What an attacker could do with this vulnerability
4. **Environment**: Charon version, deployment method, OS, etc.
5. **Proof of Concept**: Code or commands demonstrating the vulnerability (non-destructive)
6. **Suggested Fix**: If you have ideas for remediation
**Who**
- Discovered by: Automated scan (Grype)
- Reported: 2026-03-20
- Affects: CrowdSec Agent component within the container; not directly exposed through Charon's
primary application interface
### What Happens Next
**Where**
- Component: CrowdSec Agent (bundled `cscli` and `crowdsec` binaries)
- Versions affected: Charon container images with CrowdSec binaries compiled against Go < 1.25.7
1. **Acknowledgment**: We'll acknowledge your report within **48 hours**
2. **Investigation**: We'll investigate and assess the severity
3. **Updates**: We'll provide regular status updates (weekly minimum)
4. **Fix Development**: We'll develop and test a fix
5. **Disclosure**: Coordinated disclosure after fix is released
6. **Credit**: We'll credit you in release notes (if desired)
**When**
- Discovered: 2026-03-20
- Disclosed (if public): Not yet publicly disclosed
- Target fix: When `golang:1.26.2-alpine` is published on Docker Hub
### Responsible Disclosure
**How**
The vulnerability resides entirely within CrowdSec's compiled binary artifacts. Exploitation
is limited to the CrowdSec agent's internal execution paths, which are not externally exposed
through Charon's API or network interface.
We ask that you:
**Planned Remediation**
`golang:1.26.2-alpine` is not yet available on Docker Hub. The `GO_VERSION` ARG has been
reverted to `1.26.1` (the latest published image) until `1.26.2` is released. Once
`golang:1.26.2-alpine` is available, bumping `GO_VERSION` to `1.26.2` and rebuilding the image
will also resolve CVE-2026-25679 (High) and CVE-2025-61732 (High) tracked under CHARON-2025-001.
- ✅ Give us reasonable time to fix the issue before public disclosure (90 days preferred)
- ✅ Avoid destructive testing or attacks on production systems
- ✅ Not access, modify, or delete data that doesn't belong to you
- ✅ Not perform actions that could degrade service for others
---
We commit to:
### [HIGH] CVE-2026-2673 · OpenSSL TLS 1.3 Key Exchange Group Downgrade
- ✅ Respond to your report within 48 hours
- ✅ Provide regular status updates
- ✅ Credit you in release notes (if desired)
- ✅ Not pursue legal action for good-faith security research
| Field | Value |
|--------------|-------|
| **ID** | CVE-2026-2673 (affects `libcrypto3` and `libssl3`) |
| **Severity** | High · 7.5 |
| **Status** | Awaiting Upstream |
**What**
An OpenSSL TLS 1.3 server may fail to negotiate the intended key exchange group when the
configuration includes the `DEFAULT` keyword, potentially allowing downgrade to weaker cipher
suites. Affects Alpine 3.23.3 packages `libcrypto3` and `libssl3` at version 3.5.5-r0.
**Who**
- Discovered by: Automated scan (Grype)
- Reported: 2026-03-20
- Affects: Container runtime environment; Caddy reverse proxy TLS negotiation could be affected
if default key group configuration is used
**Where**
- Component: Alpine 3.23.3 base image (`libcrypto3` 3.5.5-r0, `libssl3` 3.5.5-r0)
- Versions affected: Alpine 3.23.3 prior to a patched `openssl` APK release
**When**
- Discovered: 2026-03-20
- Disclosed (if public): 2026-03-13 (OpenSSL advisory)
- Target fix: When Alpine Security publishes a patched `openssl` APK
**How**
When an OpenSSL TLS 1.3 server configuration uses the `DEFAULT` keyword for key exchange groups,
the negotiation logic may select a weaker group than intended. Charon's Caddy TLS configuration
does not use the `DEFAULT` keyword, which limits practical exploitability. The packages are
present in the base image regardless of Caddy's configuration.
**Planned Remediation**
Monitor https://security.alpinelinux.org/vuln/CVE-2026-2673 for a patched Alpine APK. Once
available, update the pinned `ALPINE_IMAGE` digest in the Dockerfile, or add an explicit
`RUN apk upgrade --no-cache libcrypto3 libssl3` to the runtime stage.
---
### [HIGH] CHARON-2025-001 · CrowdSec Bundled Binaries — Go Stdlib CVEs
| Field | Value |
|--------------|-------|
| **ID** | CHARON-2025-001 (aliases: CVE-2025-58183, CVE-2025-58186, CVE-2025-58187, CVE-2025-61729, CVE-2026-25679, CVE-2025-61732, CVE-2026-27142, CVE-2026-27139) |
| **Severity** | High · (preliminary, CVSS scores pending upstream confirmation) |
| **Status** | Awaiting Upstream |
**What**
Multiple CVEs in Go standard library packages continue to accumulate in CrowdSec binaries bundled
with Charon. The cluster originated when CrowdSec was compiled against Go 1.25.1; subsequent
CrowdSec updates advanced the toolchain to Go 1.25.6/1.25.7, resolving earlier CVEs but
introducing new ones. The cluster now includes a Critical-severity finding (CVE-2025-68121,
tracked separately above). All issues resolve when CrowdSec is rebuilt against Go ≥ 1.26.2.
Charon's own application code is unaffected.
**Who**
- Discovered by: Automated scan (Trivy, Grype)
- Reported: 2025-12-01 (original cluster); expanded 2026-03-20
- Affects: CrowdSec Agent component within the container; not directly exposed through Charon's
primary application interface
**Where**
- Component: CrowdSec Agent (bundled `cscli` and `crowdsec` binaries)
- Versions affected: All Charon versions shipping CrowdSec binaries compiled against Go < 1.26.2
**When**
- Discovered: 2025-12-01
- Disclosed (if public): Not yet publicly disclosed
- Target fix: When `golang:1.26.2-alpine` is published on Docker Hub
**How**
The CVEs reside entirely within CrowdSec's compiled binaries and cover HTTP/2, TLS, and archive
processing paths that are not invoked by Charon's core application logic. The relevant network
interfaces are not externally exposed via Charon's API surface.
**Planned Remediation**
`golang:1.26.2-alpine` is not yet available on Docker Hub. The `GO_VERSION` ARG has been
reverted to `1.26.1` (the latest published image) until `1.26.2` is released. Once available,
bumping `GO_VERSION` to `1.26.2` and rebuilding the image will resolve the entire alias cluster.
CVE-2025-68121 (Critical severity, same root cause) is tracked separately above.
---
### [MEDIUM] CVE-2026-27171 · zlib CPU Exhaustion via Infinite Loop in CRC Combine Functions
| Field | Value |
|--------------|-------|
| **ID** | CVE-2026-27171 |
| **Severity** | Medium · 5.5 (NVD) / 2.9 (MITRE) |
| **Status** | Awaiting Upstream |
**What**
zlib before 1.3.2 allows unbounded CPU consumption (denial of service) via the `crc32_combine64`
and `crc32_combine_gen64` functions. An internal helper `x2nmodp` performs right-shifts inside a
loop with no termination condition when given a specially crafted input, causing a CPU spin
(CWE-1284).
**Who**
- Discovered by: 7aSecurity audit (commissioned by OSTIF)
- Reported: 2026-02-17
- Affects: Any component in the container that calls `crc32_combine`-family functions with
attacker-controlled input; not directly exposed through Charon's application interface
**Where**
- Component: Alpine 3.23.3 base image (`zlib` package, version 1.3.1-r2)
- Versions affected: zlib < 1.3.2; all current Charon images using Alpine 3.23.3
**When**
- Discovered: 2026-02-17 (NVD published 2026-02-17)
- Disclosed (if public): 2026-02-17
- Target fix: When Alpine 3.23 publishes a patched `zlib` APK (requires zlib 1.3.2)
**How**
Exploitation requires local access (CVSS vector `AV:L`) and the ability to pass a crafted value
to the `crc32_combine`-family functions. This code path is not invoked by Charon's reverse proxy
or backend API. The vulnerability is non-blocking under the project's CI severity policy.
**Planned Remediation**
Monitor https://security.alpinelinux.org/vuln/CVE-2026-27171 for a patched Alpine APK. Once
available, update the pinned `ALPINE_IMAGE` digest in the Dockerfile, or add an explicit
`RUN apk upgrade --no-cache zlib` to the runtime stage. Remove the `.trivyignore` entry at
that time.
---
## Patched Vulnerabilities
### ✅ [HIGH] CHARON-2026-001 · Debian Base Image CVE Cluster
| Field | Value |
|--------------|-------|
| **ID** | CHARON-2026-001 (aliases: CVE-2026-0861, CVE-2025-15281, CVE-2026-0915, CVE-2025-13151, and 2 libtiff HIGH CVEs) |
| **Severity** | High · 8.4 (highest per CVSS v3.1) |
| **Patched** | 2026-03-20 (Alpine base image migration complete) |
**What**
Seven HIGH-severity CVEs in Debian Trixie base image system libraries (`glibc`, `libtasn1-6`,
`libtiff`). These vulnerabilities resided in the container's OS-level packages with no fixes
available from the Debian Security Team.
**Who**
- Discovered by: Automated scan (Trivy)
- Reported: 2026-02-04
**Where**
- Component: Debian Trixie base image (`libc6`, `libc-bin`, `libtasn1-6`, `libtiff`)
- Versions affected: Charon container images built on Debian Trixie base (prior to Alpine migration)
**When**
- Discovered: 2026-02-04
- Patched: 2026-03-20
- Time to patch: 44 days
**How**
The affected packages were OS-level shared libraries bundled in the Debian Trixie container base
image. Exploitation would have required local container access or a prior application-level
compromise. Caddy reverse proxy ingress filtering and container isolation significantly reduced
the effective attack surface throughout the exposure window.
**Resolution**
Reverted to Alpine Linux base image (Alpine 3.23.3). Alpine's patch of CVE-2025-60876 (busybox
heap overflow) removed the original blocker for the Alpine migration. Post-migration scan
confirmed zero HIGH/CRITICAL CVEs from this cluster.
- Spec: [docs/plans/alpine_migration_spec.md](docs/plans/alpine_migration_spec.md)
- Advisory: [docs/security/advisory_2026-02-04_debian_cves_temporary.md](docs/security/advisory_2026-02-04_debian_cves_temporary.md)
**Credit**
Internal remediation; no external reporter.
---
### ✅ [HIGH] CVE-2025-68156 · expr-lang/expr ReDoS
| Field | Value |
|--------------|-------|
| **ID** | CVE-2025-68156 |
| **Severity** | High · 7.5 |
| **Patched** | 2026-01-11 |
**What**
Regular Expression Denial of Service (ReDoS) vulnerability in the `expr-lang/expr` library used
by CrowdSec for expression evaluation. Malicious regular expressions in CrowdSec scenarios or
parsers could cause CPU exhaustion and service degradation through exponential backtracking.
**Who**
- Discovered by: Automated scan (Trivy)
- Reported: 2026-01-11
**Where**
- Component: CrowdSec (via `expr-lang/expr` dependency)
- Versions affected: CrowdSec versions using `expr-lang/expr` < v1.17.7
**When**
- Discovered: 2026-01-11
- Patched: 2026-01-11
- Time to patch: 0 days
**How**
Maliciously crafted regular expressions in CrowdSec scenario or parser rules could trigger
exponential backtracking in `expr-lang/expr`'s evaluation engine, causing CPU exhaustion and
denial of service. The vulnerability is in the upstream expression evaluation library, not in
Charon's own code.
**Resolution**
Upgraded CrowdSec to build from source with the patched `expr-lang/expr` v1.17.7. Verification
confirmed via `go version -m ./cscli` showing the patched library version in compiled artifacts.
Post-patch Trivy scan reports 0 HIGH/CRITICAL vulnerabilities in application code.
- Technical details: [docs/plans/crowdsec_source_build.md](docs/plans/crowdsec_source_build.md)
**Credit**
Internal remediation; no external reporter.
---
@@ -72,7 +290,8 @@ We commit to:
### Server-Side Request Forgery (SSRF) Protection
Charon implements industry-leading **5-layer defense-in-depth** SSRF protection to prevent attackers from using the application to access internal resources or cloud metadata.
Charon implements industry-leading **5-layer defense-in-depth** SSRF protection to prevent
attackers from using the application to access internal resources or cloud metadata.
#### Protected Against
@@ -100,8 +319,6 @@ Charon implements industry-leading **5-layer defense-in-depth** SSRF protection
#### Learn More
For complete technical details, see:
- [SSRF Protection Guide](docs/security/ssrf-protection.md)
- [Manual Test Plan](docs/issues/ssrf-manual-test-plan.md)
- [QA Audit Report](docs/reports/qa_ssrf_remediation_report.md)
@@ -124,7 +341,10 @@ For complete technical details, see:
### Infrastructure Security
- **Non-root by default**: Charon runs as an unprivileged user (`charon`, uid 1000) inside the container. Docker socket access is granted via a minimal supplemental group matching the host socket's GID—never by running as root. If the socket GID is `0` (root group), Charon requires explicit opt-in before granting access.
- **Non-root by default**: Charon runs as an unprivileged user (`charon`, uid 1000) inside the
container. Docker socket access is granted via a minimal supplemental group matching the host
socket's GID — never by running as root. If the socket GID is `0` (root group), Charon requires
explicit opt-in before granting access.
- **Container isolation**: Docker-based deployment
- **Minimal attack surface**: Alpine Linux base image
- **Dependency scanning**: Regular Trivy and govulncheck scans
@@ -139,6 +359,126 @@ For complete technical details, see:
---
## Supply Chain Security
Charon implements comprehensive supply chain security measures to ensure the integrity and
authenticity of releases. Every release includes cryptographic signatures, SLSA provenance
attestation, and a Software Bill of Materials (SBOM).
### Verification Commands
#### Verify Container Image Signature
All official Charon images are signed with Sigstore Cosign:
```bash
cosign verify \
--certificate-identity-regexp='https://github.com/Wikid82/charon' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
ghcr.io/wikid82/charon:latest
```
Successful verification confirms the image was built by GitHub Actions from the official
repository and has not been tampered with since signing.
#### Verify SLSA Provenance
```bash
# Download provenance from release assets
curl -LO https://github.com/Wikid82/charon/releases/latest/download/provenance.json
slsa-verifier verify-artifact \
--provenance-path provenance.json \
--source-uri github.com/Wikid82/charon \
./backend/charon-binary
```
#### Inspect the SBOM
```bash
# Download SBOM from release assets
curl -LO https://github.com/Wikid82/charon/releases/latest/download/sbom.spdx.json
# Scan for known vulnerabilities
grype sbom:sbom.spdx.json
```
### Transparency Log (Rekor)
All signatures are recorded in the public Sigstore Rekor transparency log:
<https://search.sigstore.dev/>
### Digest Pinning Policy
**Scope (Required):**
- CI workflows: `.github/workflows/*.yml`
- CI compose files: `.docker/compose/*.yml`
- CI helper actions with container refs: `.github/actions/**/*.yml`
CI workflows and CI compose files MUST use digest-pinned images for third-party services.
Tag+digest pairs are preferred for human-readable references with immutable resolution.
Self-built images MUST propagate digests to downstream jobs and tests.
**Local Development Exceptions:**
Local-only overrides (e.g., `CHARON_E2E_IMAGE`, `CHARON_IMAGE`, `CHARON_DEV_IMAGE`) MAY use tags
for developer iteration. Tag-only overrides MUST NOT be used in CI contexts.
**Documented Exceptions & Compensating Controls:**
1. **Go toolchain shim** (`golang.org/dl/goX.Y.Z@latest`) — Uses `@latest` to install the shim;
compensated by the target toolchain version being pinned in `go.work` with Renovate tracking.
2. **Unpinnable dependencies** — Require documented justification; prefer vendor checksums or
signed releases; keep SBOM/vulnerability scans in CI.
### Learn More
- [User Guide](docs/guides/supply-chain-security-user-guide.md)
- [Developer Guide](docs/guides/supply-chain-security-developer-guide.md)
- [Sigstore Documentation](https://docs.sigstore.dev/)
- [SLSA Framework](https://slsa.dev/)
---
## Security Audits & Scanning
### Automated Scanning
| Tool | Purpose |
|------|---------|
| Trivy | Container image vulnerability scanning |
| CodeQL | Static analysis for Go and JavaScript |
| govulncheck | Go module vulnerability scanning |
| golangci-lint (gosec) | Go code linting |
| npm audit | Frontend dependency scanning |
### Scanning Workflows
**Docker Build & Scan** (`.github/workflows/docker-build.yml`) — runs on every commit to `main`,
`development`, and `feature/beta-release`, and on all PRs targeting those branches. Performs Trivy
scanning, generates an SBOM, creates SBOM attestations, and uploads SARIF results to the GitHub
Security tab.
**Supply Chain Verification** (`.github/workflows/supply-chain-verify.yml`) — triggers
automatically via `workflow_run` after a successful docker-build. Runs SBOM completeness checks,
Grype vulnerability scans, and (on releases) Cosign signature and SLSA provenance validation.
**Weekly Security Rebuild** (`.github/workflows/security-weekly-rebuild.yml`) — runs every Sunday
at 02:00 UTC. Performs a full no-cache rebuild, scans for all severity levels, and retains JSON
artifacts for 90 days.
**PR-Specific Scanning** — extracts and scans only the Charon application binary on each pull
request. Fails the PR if CRITICAL or HIGH vulnerabilities are found in application code.
### Manual Reviews
- Security code reviews for all major features
- Peer review of security-sensitive changes
- Third-party security audits (planned)
---
## Security Best Practices
### Deployment Recommendations
@@ -153,26 +493,25 @@ For complete technical details, see:
### Configuration Hardening
```yaml
# Recommended docker-compose.yml settings
services:
charon:
image: ghcr.io/wikid82/charon:latest
restart: unless-stopped
environment:
- CHARON_ENV=production
- LOG_LEVEL=info # Don't use debug in production
- LOG_LEVEL=info
volumes:
- ./charon-data:/app/data:rw
- /var/run/docker.sock:/var/run/docker.sock:ro # Read-only!
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- charon-internal # Isolated network
- charon-internal
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if binding to ports < 1024
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
read_only: true # If possible
read_only: true
tmpfs:
- /tmp:noexec,nosuid,nodev
```
@@ -182,9 +521,8 @@ services:
Gotify application tokens are secrets and must be handled with strict confidentiality.
- Never echo, print, log, or return token values in API responses or errors.
- Never expose tokenized endpoint query strings (for example,
`...?token=...`) in logs, diagnostics, examples, screenshots,
tickets, or reports.
- Never expose tokenized endpoint query strings (e.g., `...?token=...`) in logs, diagnostics,
examples, screenshots, tickets, or reports.
- Always redact query parameters in diagnostics and examples before display or storage.
- Use write-only token inputs in operator workflows and UI forms.
- Store tokens only in environment variables or a dedicated secret manager.
@@ -200,322 +538,6 @@ Gotify application tokens are secrets and must be handled with strict confidenti
---
## Supply Chain Security
Charon implements comprehensive supply chain security measures to ensure the integrity and authenticity of releases. Every release includes cryptographic signatures, SLSA provenance attestation, and Software Bill of Materials (SBOM).
### Verification Commands
#### Verify Container Image Signature
All official Charon images are signed with Sigstore Cosign:
```bash
# Install cosign (if not already installed)
curl -LO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
sudo chmod +x /usr/local/bin/cosign
# Verify image signature
cosign verify \
--certificate-identity-regexp='https://github.com/Wikid82/charon' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
ghcr.io/wikid82/charon:latest
```
Successful verification output confirms:
- The image was built by GitHub Actions
- The build came from the official Charon repository
- The image has not been tampered with since signing
#### Verify SLSA Provenance
SLSA (Supply-chain Levels for Software Artifacts) provenance provides tamper-proof evidence of how the software was built:
```bash
# Install slsa-verifier (if not already installed)
curl -LO https://github.com/slsa-framework/slsa-verifier/releases/latest/download/slsa-verifier-linux-amd64
sudo mv slsa-verifier-linux-amd64 /usr/local/bin/slsa-verifier
sudo chmod +x /usr/local/bin/slsa-verifier
# Download provenance from release assets
curl -LO https://github.com/Wikid82/charon/releases/latest/download/provenance.json
# Verify provenance
slsa-verifier verify-artifact \
--provenance-path provenance.json \
--source-uri github.com/Wikid82/charon \
./backend/charon-binary
```
#### Inspect Software Bill of Materials (SBOM)
Every release includes a comprehensive SBOM in SPDX format:
```bash
# Download SBOM from release assets
curl -LO https://github.com/Wikid82/charon/releases/latest/download/sbom.spdx.json
# View SBOM contents
cat sbom.spdx.json | jq .
# Check for known vulnerabilities (requires Grype)
grype sbom:sbom.spdx.json
```
### Transparency Log (Rekor)
All signatures are recorded in the public Sigstore Rekor transparency log, providing an immutable audit trail:
- **Search the log**: <https://search.sigstore.dev/>
- **Query by image**: Search for `ghcr.io/wikid82/charon`
- **View entry details**: Each entry includes commit SHA, workflow run, and signing timestamp
### Automated Verification in CI/CD
Integrate supply chain verification into your deployment pipeline:
```yaml
# Example GitHub Actions workflow
- name: Verify Charon Image
run: |
cosign verify \
--certificate-identity-regexp='https://github.com/Wikid82/charon' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
ghcr.io/wikid82/charon:${{ env.VERSION }}
```
### What's Protected
- **Container Images**: All `ghcr.io/wikid82/charon:*` images are signed
- **Release Binaries**: Backend binaries include provenance attestation
- **Build Process**: SLSA Level 3 compliant build provenance
- **Dependencies**: Complete SBOM including all direct and transitive dependencies
### Digest Pinning Policy
Charon uses digest pinning to reduce supply chain risk and ensure CI runs against immutable artifacts.
**Scope (Required):**
- **CI workflows**: `.github/workflows/*.yml`, `.github/workflows/*.yaml`
- **CI compose files**: `.docker/compose/*.yml`, `.docker/compose/*.yaml`, `.docker/compose/docker-compose*.yml`, `.docker/compose/docker-compose*.yaml`
- **CI helper actions with container refs**: `.github/actions/**/*.yml`, `.github/actions/**/*.yaml`
- CI workflows and CI compose files MUST use digest-pinned images for third-party services.
- Tag+digest pairs are preferred for human-readable references with immutable resolution.
- Self-built images MUST propagate digests to downstream jobs and tests.
**Rationale:**
- Prevent tag drift and supply chain substitution in automated runs.
- Ensure deterministic builds, reproducible scans, and stable SBOM generation.
- Reduce rollback risk by guaranteeing CI uses immutable artifacts.
**Local Development Exceptions:**
- Local-only overrides (e.g., `CHARON_E2E_IMAGE`, `CHARON_IMAGE`, `CHARON_DEV_IMAGE`) MAY use tags for developer iteration.
- Tag-only overrides MUST NOT be used in CI contexts.
**Documented Exceptions & Compensating Controls:**
1. **Go toolchain shim** (`golang.org/dl/goX.Y.Z@latest`)
- **Exception:** Uses `@latest` to install the shim.
- **Compensating controls:** The target toolchain version is pinned in
`go.work`, and Renovate tracks the required version for updates.
2. **Unpinnable dependencies** (no stable digest or checksum source)
- **Exception:** Dependency cannot be pinned by digest.
- **Compensating controls:** Require documented justification, prefer
vendor-provided checksums or signed releases when available, and keep
SBOM/vulnerability scans in CI.
### Learn More
- **[User Guide](docs/guides/supply-chain-security-user-guide.md)**: Step-by-step verification instructions
- **[Developer Guide](docs/guides/supply-chain-security-developer-guide.md)**: Integration into development workflow
- **[Sigstore Documentation](https://docs.sigstore.dev/)**: Technical details on signing and verification
- **[SLSA Framework](https://slsa.dev/)**: Supply chain security framework overview
---
## Security Audits & Scanning
### Automated Scanning
We use the following tools:
- **Trivy**: Container image vulnerability scanning
- **CodeQL**: Static code analysis for Go and JavaScript
- **govulncheck**: Go module vulnerability scanning
- **golangci-lint**: Go code linting (including gosec)
- **npm audit**: Frontend dependency vulnerability scanning
### Security Scanning Workflows
Charon implements multiple layers of automated security scanning:
#### Docker Build & Scan (Per-Commit)
**Workflow**: `.github/workflows/docker-build.yml`
- Runs on every commit to `main`, `development`, and `feature/beta-release` branches
- Runs on all pull requests targeting these branches
- Performs Trivy vulnerability scanning on built images
- Generates SBOM (Software Bill of Materials) for supply chain transparency
- Creates SBOM attestations for verifiable build provenance
- Verifies Caddy security patches (CVE-2025-68156)
- Uploads SARIF results to GitHub Security tab
**Note**: This workflow replaced the previous `docker-publish.yml` (deleted Dec 21, 2025) with enhanced security features.
#### Supply Chain Verification
**Workflow**: `.github/workflows/supply-chain-verify.yml`
**Trigger Timing**: Runs automatically after `docker-build.yml` completes successfully via `workflow_run` trigger.
**Branch Coverage**: Triggers on **ALL branches** where docker-build completes, including:
- `main` (default branch)
- `development`
- `feature/*` branches (including `feature/beta-release`)
- Pull request branches
**Why No Branch Filter**: GitHub Actions has a platform limitation where `branches` filters in `workflow_run` triggers only match the default branch. To ensure comprehensive supply chain verification across all branches and PRs, we intentionally omit the branch filter. The workflow file must exist on the branch to execute, preventing untrusted code execution.
**Verification Steps**:
1. SBOM completeness verification
2. Vulnerability scanning with Grype
3. Results uploaded as workflow artifacts
4. PR comments with vulnerability summary (when applicable)
5. For releases: Cosign signature verification and SLSA provenance validation
**Additional Triggers**:
- Runs on all published releases
- Scheduled weekly on Mondays at 00:00 UTC
- Can be triggered manually via `workflow_dispatch`
#### Weekly Security Rebuild
**Workflow**: `.github/workflows/security-weekly-rebuild.yml`
- Runs every Sunday at 02:00 UTC
- Performs full rebuild with no cache to ensure latest base images
- Scans with Trivy for CRITICAL, HIGH, MEDIUM, and LOW vulnerabilities
- Uploads results to GitHub Security tab
- Stores JSON artifacts for 90-day retention
- Checks Alpine package versions for security updates
#### PR-Specific Scanning
**Workflow**: `.github/workflows/docker-build.yml` (trivy-pr-app-only job)
- Runs on all pull requests
- Extracts and scans only the Charon application binary
- Fails PR if CRITICAL or HIGH vulnerabilities found in application code
- Faster feedback loop for developers during code review
### Workflow Orchestration
The security scanning workflows use a coordinated orchestration pattern:
1. **Build Phase**: `docker-build.yml` builds the image and performs initial Trivy scan
2. **Verification Phase**: `supply-chain-verify.yml` triggers automatically via `workflow_run` after successful build
3. **Verification Timing**:
- On feature branches: Runs after docker-build completes on push events
- On pull requests: Runs after docker-build completes on PR synchronize events
- No delay or gaps: verification starts immediately after build success
4. **Weekly Maintenance**: `security-weekly-rebuild.yml` provides ongoing monitoring
This pattern ensures:
- Images are built before verification attempts to scan them
- No race conditions between build and verification
- Comprehensive coverage across all branches and PRs
- Efficient resource usage (verification only runs after successful builds)
### Manual Reviews
- Security code reviews for all major features
- Peer review of security-sensitive changes
- Third-party security audits (planned)
### Continuous Monitoring
- GitHub Dependabot alerts
- Weekly security scans in CI/CD
- Community vulnerability reports
- Automated supply chain verification on every build
---
## Recently Resolved Vulnerabilities
Charon maintains transparency about security issues and their resolution. Below is a comprehensive record of recently patched vulnerabilities.
### CVE-2025-68156 (expr-lang/expr ReDoS)
- **Severity**: HIGH (CVSS 7.5)
- **Component**: expr-lang/expr (used by CrowdSec for expression evaluation)
- **Vulnerability**: Regular Expression Denial of Service (ReDoS)
- **Description**: Malicious regular expressions in CrowdSec scenarios or parsers could cause CPU exhaustion and service degradation through exponential backtracking in vulnerable regex patterns.
- **Fixed Version**: expr-lang/expr v1.17.7
- **Resolution Date**: January 11, 2026
- **Remediation**: Upgraded CrowdSec to build from source with patched expr-lang/expr v1.17.7
- **Verification**:
- Binary inspection: `go version -m ./cscli` confirms v1.17.7 in compiled artifacts
- Container scan: Trivy reports 0 HIGH/CRITICAL vulnerabilities in application code
- Runtime testing: CrowdSec scenarios and parsers load successfully with patched library
- **Impact**: No known exploits in Charon deployments; preventive upgrade completed
- **Status**: ✅ **PATCHED** — Verified in all release artifacts
- **Technical Details**: See [CrowdSec Source Build Documentation](docs/plans/crowdsec_source_build.md)
---
## Known Security Considerations
### Debian Base Image CVEs (2026-02-04) — TEMPORARY
**Status**: ⚠️ 7 HIGH severity CVEs in Debian Trixie base image. **Alpine migration in progress.**
**Background**: Migrated from Alpine → Debian due to CVE-2025-60876 (busybox heap overflow). Debian now has worse CVE posture with no fixes available. Reverting to Alpine as Alpine CVE-2025-60876 is now patched.
**Affected Packages**:
- **libc6/libc-bin** (glibc): CVE-2026-0861 (CVSS 8.4), CVE-2025-15281, CVE-2026-0915
- **libtasn1-6**: CVE-2025-13151 (CVSS 7.5)
- **libtiff**: 2 additional HIGH CVEs
**Fix Status**: ❌ No fixes available from Debian Security Team
**Risk Assessment**: 🟢 **LOW actual risk**
- CVEs affect system libraries, NOT Charon application code
- Container isolation limits exploit surface area
- No direct exploit paths identified in Charon's usage patterns
- Network ingress filtered through Caddy proxy
**Mitigation**: Alpine base image migration
- **Spec**: [`docs/plans/alpine_migration_spec.md`](docs/plans/alpine_migration_spec.md)
- **Security Advisory**: [`docs/security/advisory_2026-02-04_debian_cves_temporary.md`](docs/security/advisory_2026-02-04_debian_cves_temporary.md)
- **Timeline**: 2-3 weeks (target completion: March 5, 2026)
- **Expected Outcome**: 100% CVE reduction (7 HIGH → 0)
**Review Date**: 2026-02-11 (Phase 1 Alpine CVE verification)
**Details**: See [VULNERABILITY_ACCEPTANCE.md](docs/security/VULNERABILITY_ACCEPTANCE.md) for complete risk assessment and monitoring plan.
### Third-Party Dependencies
**CrowdSec Binaries**: As of December 2025, CrowdSec binaries shipped with Charon contain 4 HIGH-severity CVEs in Go stdlib (CVE-2025-58183, CVE-2025-58186, CVE-2025-58187, CVE-2025-61729). These are upstream issues in Go 1.25.1 and will be resolved when CrowdSec releases binaries built with go 1.26.0+.
**Impact**: Low. These vulnerabilities are in CrowdSec's third-party binaries, not in Charon's application code. They affect HTTP/2, TLS certificate handling, and archive parsing—areas not directly exposed to attackers through Charon's interface.
**Mitigation**: Monitor CrowdSec releases for updated binaries. Charon's own application code has zero vulnerabilities.
---
## Security Hall of Fame
We recognize security researchers who help improve Charon:
@@ -525,19 +547,4 @@ We recognize security researchers who help improve Charon:
---
## Security Contact
- **GitHub Security Advisories**: <https://github.com/Wikid82/charon/security/advisories>
- **GitHub Discussions**: <https://github.com/Wikid82/charon/discussions>
- **GitHub Issues** (non-security): <https://github.com/Wikid82/charon/issues>
---
## License
This security policy is part of the Charon project, licensed under the MIT License.
---
**Last Updated**: January 30, 2026
**Version**: 1.2
**Last Updated**: 2026-03-20

View File

@@ -1,6 +1,6 @@
module github.com/Wikid82/charon/backend
go 1.26
go 1.26.1
require (
github.com/docker/docker v28.5.2+incompatible
@@ -10,16 +10,16 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/mattn/go-sqlite3 v1.14.34
github.com/mattn/go-sqlite3 v1.14.37
github.com/oschwald/geoip2-golang/v2 v2.1.0
github.com/prometheus/client_golang v1.23.2
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.48.0
golang.org/x/net v0.51.0
golang.org/x/text v0.34.0
golang.org/x/time v0.14.0
golang.org/x/crypto v0.49.0
golang.org/x/net v0.52.0
golang.org/x/text v0.35.0
golang.org/x/time v0.15.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
@@ -28,7 +28,7 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -50,7 +50,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@@ -64,7 +64,7 @@ require (
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/morikuni/aec v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -79,24 +79,25 @@ require (
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/arch v0.24.0 // indirect
golang.org/x/sys v0.41.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/sys v0.42.0 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.2 // indirect
modernc.org/libc v1.69.0 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.1 // indirect
modernc.org/sqlite v1.47.0 // indirect
)

View File

@@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
@@ -62,8 +62,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -77,8 +77,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -101,8 +101,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
@@ -116,8 +116,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
@@ -159,8 +159,9 @@ github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC4
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -176,55 +177,55 @@ go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=
go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -243,8 +244,8 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.31.0 h1:/bsaxqdgX3gy/0DboxcvWrc3NpzH+6wpFfI/ZaA/hrg=
modernc.org/ccgo/v4 v4.31.0/go.mod h1:jKe8kPBjIN/VdGTVqARTQ8N1gAziBmiISY8j5HoKwjg=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
@@ -253,8 +254,8 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.69.0 h1:YQJ5QMSReTgQ3QFmI0dudfjXIjCcYTUxcH8/9P9f0D8=
modernc.org/libc v1.69.0/go.mod h1:YfLLduUEbodNV2xLU5JOnRHBTAHVHsVW3bVYGw0ZCV4=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -263,8 +264,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -63,7 +63,10 @@ func (h *AuditLogHandler) List(c *gin.Context) {
}
// Calculate pagination metadata
totalPages := (int(total) + limit - 1) / limit
var totalPages int
if limit > 0 {
totalPages = (int(total) + limit - 1) / limit
}
c.JSON(http.StatusOK, gin.H{
"audit_logs": audits,
@@ -127,7 +130,10 @@ func (h *AuditLogHandler) ListByProvider(c *gin.Context) {
}
// Calculate pagination metadata
totalPages := (int(total) + limit - 1) / limit
var totalPages int
if limit > 0 {
totalPages = (int(total) + limit - 1) / limit
}
c.JSON(http.StatusOK, gin.H{
"audit_logs": audits,

View File

@@ -77,12 +77,12 @@ func originHost(rawURL string) string {
return normalizeHost(parsedURL.Host)
}
func isLocalHost(host string) bool {
func isLocalOrPrivateHost(host string) bool {
if strings.EqualFold(host, "localhost") {
return true
}
if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() {
if ip := net.ParseIP(host); ip != nil && (ip.IsLoopback() || ip.IsPrivate()) {
return true
}
@@ -117,7 +117,7 @@ func isLocalRequest(c *gin.Context) bool {
continue
}
if isLocalHost(host) {
if isLocalOrPrivateHost(host) {
return true
}
}
@@ -127,17 +127,15 @@ func isLocalRequest(c *gin.Context) bool {
// setSecureCookie sets an auth cookie with security best practices
// - HttpOnly: prevents JavaScript access (XSS protection)
// - Secure: true for HTTPS; false only for local non-HTTPS loopback flows
// - SameSite: Strict for HTTPS, Lax for HTTP/IP to allow forward-auth redirects
// - Secure: always true (all major browsers honour Secure on localhost HTTP;
// HTTP-on-private-IP without TLS is an unsupported deployment)
// - SameSite: Lax for any local/private-network request (regardless of scheme),
// Strict otherwise (public HTTPS only)
func setSecureCookie(c *gin.Context, name, value string, maxAge int) {
scheme := requestScheme(c)
secure := true
sameSite := http.SameSiteStrictMode
if scheme != "https" {
sameSite = http.SameSiteLaxMode
if isLocalRequest(c) {
secure = false
}
}
if isLocalRequest(c) {
@@ -154,7 +152,7 @@ func setSecureCookie(c *gin.Context, name, value string, maxAge int) {
maxAge, // maxAge in seconds
"/", // path
domain, // domain (empty = current host)
secure, // secure (always true)
true, // secure
true, // httpOnly (no JS access)
)
}

View File

@@ -112,7 +112,7 @@ func TestSetSecureCookie_HTTP_Loopback_Insecure(t *testing.T) {
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.False(t, cookie.Secure)
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
@@ -202,6 +202,114 @@ func TestSetSecureCookie_OriginLoopbackForcesInsecure(t *testing.T) {
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestSetSecureCookie_HTTP_PrivateIP_Insecure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "http://192.168.1.50:8080/login", http.NoBody)
req.Host = "192.168.1.50:8080"
req.Header.Set("X-Forwarded-Proto", "http")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestSetSecureCookie_HTTP_10Network_Insecure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "http://10.0.0.5:8080/login", http.NoBody)
req.Host = "10.0.0.5:8080"
req.Header.Set("X-Forwarded-Proto", "http")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestSetSecureCookie_HTTP_172Network_Insecure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "http://172.16.0.1:8080/login", http.NoBody)
req.Host = "172.16.0.1:8080"
req.Header.Set("X-Forwarded-Proto", "http")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestSetSecureCookie_HTTPS_PrivateIP_Secure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "https://192.168.1.50:8080/login", http.NoBody)
req.Host = "192.168.1.50:8080"
req.Header.Set("X-Forwarded-Proto", "https")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestSetSecureCookie_HTTP_IPv6ULA_Insecure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "http://[fd12::1]:8080/login", http.NoBody)
req.Host = "[fd12::1]:8080"
req.Header.Set("X-Forwarded-Proto", "http")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestSetSecureCookie_HTTP_PublicIP_Secure(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest("POST", "http://203.0.113.5:8080/login", http.NoBody)
req.Host = "203.0.113.5:8080"
req.Header.Set("X-Forwarded-Proto", "http")
ctx.Request = req
setSecureCookie(ctx, "auth_token", "abc", 60)
cookies := recorder.Result().Cookies()
require.Len(t, cookies, 1)
cookie := cookies[0]
assert.True(t, cookie.Secure)
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
}
func TestIsProduction(t *testing.T) {
t.Setenv("CHARON_ENV", "production")
assert.True(t, isProduction())
@@ -271,11 +379,16 @@ func TestHostHelpers(t *testing.T) {
assert.Equal(t, "localhost", originHost("http://localhost:8080/path"))
})
t.Run("isLocalHost", func(t *testing.T) {
assert.True(t, isLocalHost("localhost"))
assert.True(t, isLocalHost("127.0.0.1"))
assert.True(t, isLocalHost("::1"))
assert.False(t, isLocalHost("example.com"))
t.Run("isLocalOrPrivateHost", func(t *testing.T) {
assert.True(t, isLocalOrPrivateHost("localhost"))
assert.True(t, isLocalOrPrivateHost("127.0.0.1"))
assert.True(t, isLocalOrPrivateHost("::1"))
assert.True(t, isLocalOrPrivateHost("192.168.1.50"))
assert.True(t, isLocalOrPrivateHost("10.0.0.1"))
assert.True(t, isLocalOrPrivateHost("172.16.0.1"))
assert.True(t, isLocalOrPrivateHost("fd12::1"))
assert.False(t, isLocalOrPrivateHost("203.0.113.5"))
assert.False(t, isLocalOrPrivateHost("example.com"))
})
}
@@ -326,6 +439,7 @@ func TestClearSecureCookie(t *testing.T) {
require.Len(t, cookies, 1)
assert.Equal(t, "auth_token", cookies[0].Name)
assert.Equal(t, -1, cookies[0].MaxAge)
assert.True(t, cookies[0].Secure)
}
func TestAuthHandler_Login_Errors(t *testing.T) {
@@ -1222,10 +1336,10 @@ func TestAuthHandler_HelperFunctions(t *testing.T) {
assert.Equal(t, "example.com", originHost("https://example.com/path"))
})
t.Run("isLocalHost and isLocalRequest", func(t *testing.T) {
assert.True(t, isLocalHost("localhost"))
assert.True(t, isLocalHost("127.0.0.1"))
assert.False(t, isLocalHost("example.com"))
t.Run("isLocalOrPrivateHost and isLocalRequest", func(t *testing.T) {
assert.True(t, isLocalOrPrivateHost("localhost"))
assert.True(t, isLocalOrPrivateHost("127.0.0.1"))
assert.False(t, isLocalOrPrivateHost("example.com"))
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)

View File

@@ -41,7 +41,8 @@ func (h *CerberusLogsHandler) LiveLogs(c *gin.Context) {
logger.Log().Info("Cerberus logs WebSocket connection attempt")
// Upgrade HTTP connection to WebSocket
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
// CheckOrigin is enforced on the shared upgrader in logs_ws.go (same package).
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) // nosemgrep: go.gorilla.security.audit.websocket-missing-origin-check.websocket-missing-origin-check
if err != nil {
logger.Log().WithError(err).Error("Failed to upgrade Cerberus logs WebSocket")
return

View File

@@ -125,7 +125,7 @@ func (h *CertificateHandler) Upload(c *gin.Context) {
h.notificationService.SendExternal(c.Request.Context(),
"cert",
"Certificate Uploaded",
fmt.Sprintf("Certificate %s uploaded", util.SanitizeForLog(cert.Name)),
"A new custom certificate was successfully uploaded.",
map[string]any{
"Name": util.SanitizeForLog(cert.Name),
"Domains": util.SanitizeForLog(cert.Domains),

View File

@@ -17,6 +17,8 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@@ -516,6 +518,42 @@ func generateSelfSignedCertPEM() (certPEM, keyPEM string, err error) {
// Note: mockCertificateService removed — helper tests now use real service instances or testify mocks inlined where required.
// TestCertificateHandler_Upload_WithNotificationService verifies that the notification
// path is exercised when a non-nil NotificationService is provided.
func TestCertificateHandler_Upload_WithNotificationService(t *testing.T) {
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.Setting{}, &models.NotificationProvider{}))
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(mockAuthMiddleware())
tmpDir := t.TempDir()
svc := services.NewCertificateService(tmpDir, db)
ns := services.NewNotificationService(db, nil)
h := NewCertificateHandler(svc, nil, ns)
r.POST("/api/certificates", h.Upload)
var body bytes.Buffer
writer := multipart.NewWriter(&body)
_ = writer.WriteField("name", "cert-with-ns")
certPEM, keyPEM, err := generateSelfSignedCertPEM()
require.NoError(t, err)
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
_, _ = part.Write([]byte(certPEM))
part2, _ := writer.CreateFormFile("key_file", "key.pem")
_, _ = part2.Write([]byte(keyPEM))
_ = writer.Close()
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
}
// Test Delete with invalid ID format
func TestDeleteCertificate_InvalidID(t *testing.T) {
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
@@ -721,7 +759,7 @@ func TestDeleteCertificate_NotificationRateLimit(t *testing.T) {
r := gin.New()
r.Use(mockAuthMiddleware())
svc := services.NewCertificateService("/tmp", db)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
mockBackupService := &mockBackupService{
createFunc: func() (string, error) {

View File

@@ -1,7 +1,6 @@
package handlers
import (
"fmt"
"net/http"
"github.com/Wikid82/charon/backend/internal/models"
@@ -56,7 +55,7 @@ func (h *DomainHandler) Create(c *gin.Context) {
h.notificationService.SendExternal(c.Request.Context(),
"domain",
"Domain Added",
fmt.Sprintf("Domain %s added", util.SanitizeForLog(domain.Name)),
"A new domain was successfully added.",
map[string]any{
"Name": util.SanitizeForLog(domain.Name),
"Action": "created",
@@ -76,7 +75,7 @@ func (h *DomainHandler) Delete(c *gin.Context) {
h.notificationService.SendExternal(c.Request.Context(),
"domain",
"Domain Deleted",
fmt.Sprintf("Domain %s deleted", util.SanitizeForLog(domain.Name)),
"A domain was successfully deleted.",
map[string]any{
"Name": util.SanitizeForLog(domain.Name),
"Action": "deleted",

View File

@@ -24,7 +24,7 @@ func setupDomainTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.Domain{}, &models.Notification{}, &models.NotificationProvider{}))
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
h := NewDomainHandler(db, ns)
r := gin.New()

View File

@@ -1,7 +1,6 @@
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -127,179 +126,3 @@ func TestBlocker3_SecurityProviderEventsFlagCanBeEnabled(t *testing.T) {
assert.True(t, response["feature.notifications.security_provider_events.enabled"],
"security_provider_events flag should be true when enabled in DB")
}
// TestLegacyFallbackRemoved_UpdateFlagsRejectsTrue tests that attempting to set legacy fallback to true returns error code LEGACY_FALLBACK_REMOVED.
func TestLegacyFallbackRemoved_UpdateFlagsRejectsTrue(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
assert.NoError(t, db.AutoMigrate(&models.Setting{}))
handler := NewFeatureFlagsHandler(db)
// Attempt to set legacy fallback to true
payload := map[string]bool{
"feature.notifications.legacy.fallback_enabled": true,
}
jsonPayload, err := json.Marshal(payload)
assert.NoError(t, err)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("PUT", "/api/v1/feature-flags", bytes.NewBuffer(jsonPayload))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateFlags(c)
// Must return 400 with code LEGACY_FALLBACK_REMOVED
assert.Equal(t, http.StatusBadRequest, w.Code)
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Contains(t, response["error"], "retired")
assert.Equal(t, "LEGACY_FALLBACK_REMOVED", response["code"])
}
// TestLegacyFallbackRemoved_UpdateFlagsAcceptsFalse tests that setting legacy fallback to false is allowed (forced false).
func TestLegacyFallbackRemoved_UpdateFlagsAcceptsFalse(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
assert.NoError(t, db.AutoMigrate(&models.Setting{}))
handler := NewFeatureFlagsHandler(db)
// Set legacy fallback to false (should be accepted and forced)
payload := map[string]bool{
"feature.notifications.legacy.fallback_enabled": false,
}
jsonPayload, err := json.Marshal(payload)
assert.NoError(t, err)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("PUT", "/api/v1/feature-flags", bytes.NewBuffer(jsonPayload))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateFlags(c)
assert.Equal(t, http.StatusOK, w.Code)
// Verify in DB that it's false
var setting models.Setting
db.Where("key = ?", "feature.notifications.legacy.fallback_enabled").First(&setting)
assert.Equal(t, "false", setting.Value)
}
// TestLegacyFallbackRemoved_GetFlagsReturnsHardFalse tests that GET always returns false for legacy fallback.
func TestLegacyFallbackRemoved_GetFlagsReturnsHardFalse(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
assert.NoError(t, db.AutoMigrate(&models.Setting{}))
handler := NewFeatureFlagsHandler(db)
// Scenario 1: No DB entry
t.Run("no_db_entry", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/api/v1/feature-flags", nil)
handler.GetFlags(c)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]bool
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.False(t, response["feature.notifications.legacy.fallback_enabled"], "Must return hard-false when no DB entry")
})
// Scenario 2: DB entry says true (invalid, forced false)
t.Run("db_entry_true", func(t *testing.T) {
// Force a true value in DB (simulating legacy state)
setting := models.Setting{
Key: "feature.notifications.legacy.fallback_enabled",
Value: "true",
Type: "bool",
Category: "feature",
}
db.Create(&setting)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/api/v1/feature-flags", nil)
handler.GetFlags(c)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]bool
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.False(t, response["feature.notifications.legacy.fallback_enabled"], "Must return hard-false even when DB says true")
// Clean up
db.Unscoped().Delete(&setting)
})
// Scenario 3: DB entry says false
t.Run("db_entry_false", func(t *testing.T) {
setting := models.Setting{
Key: "feature.notifications.legacy.fallback_enabled",
Value: "false",
Type: "bool",
Category: "feature",
}
db.Create(&setting)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/api/v1/feature-flags", nil)
handler.GetFlags(c)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]bool
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.False(t, response["feature.notifications.legacy.fallback_enabled"], "Must return hard-false when DB says false")
// Clean up
db.Unscoped().Delete(&setting)
})
}
// TestLegacyFallbackRemoved_InvalidEnvValue tests that invalid environment variable values are handled (lines 157-158)
func TestLegacyFallbackRemoved_InvalidEnvValue(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
assert.NoError(t, db.AutoMigrate(&models.Setting{}))
// Set invalid environment variable value
t.Setenv("CHARON_NOTIFICATIONS_LEGACY_FALLBACK", "invalid-value")
handler := NewFeatureFlagsHandler(db)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/api/v1/feature-flags", nil)
// Lines 157-158: Should log warning for invalid env value and return hard-false
handler.GetFlags(c)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]bool
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.False(t, response["feature.notifications.legacy.fallback_enabled"], "Must return hard-false even with invalid env value")
}

View File

@@ -1,105 +0,0 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// TestResolveRetiredLegacyFallback_InvalidPersistedValue covers lines 139-140
func TestResolveRetiredLegacyFallback_InvalidPersistedValue(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.Setting{}))
// Create setting with invalid value for retired fallback flag
db.Create(&models.Setting{
Key: "feature.notifications.legacy.fallback_enabled",
Value: "invalid_value_not_bool",
Type: "bool",
Category: "feature",
})
h := NewFeatureFlagsHandler(db)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
// Should log warning and return false (lines 139-140)
var flags map[string]bool
err = json.Unmarshal(w.Body.Bytes(), &flags)
require.NoError(t, err)
assert.False(t, flags["feature.notifications.legacy.fallback_enabled"])
}
// TestResolveRetiredLegacyFallback_InvalidEnvValue covers lines 149-150
func TestResolveRetiredLegacyFallback_InvalidEnvValue(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.Setting{}))
// Set invalid env var for retired fallback flag
t.Setenv("CHARON_LEGACY_FALLBACK_ENABLED", "not_a_boolean")
h := NewFeatureFlagsHandler(db)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
// Should log warning and return false (lines 149-150)
var flags map[string]bool
err = json.Unmarshal(w.Body.Bytes(), &flags)
require.NoError(t, err)
assert.False(t, flags["feature.notifications.legacy.fallback_enabled"])
}
// TestResolveRetiredLegacyFallback_DefaultFalse covers lines 157-158
func TestResolveRetiredLegacyFallback_DefaultFalse(t *testing.T) {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.Setting{}))
// No DB value, no env vars - should default to false
h := NewFeatureFlagsHandler(db)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
// Should return false (lines 157-158)
var flags map[string]bool
err = json.Unmarshal(w.Body.Bytes(), &flags)
require.NoError(t, err)
assert.False(t, flags["feature.notifications.legacy.fallback_enabled"])
}

View File

@@ -30,9 +30,10 @@ var defaultFlags = []string{
"feature.crowdsec.console_enrollment",
"feature.notifications.engine.notify_v1.enabled",
"feature.notifications.service.discord.enabled",
"feature.notifications.service.email.enabled",
"feature.notifications.service.gotify.enabled",
"feature.notifications.service.webhook.enabled",
"feature.notifications.legacy.fallback_enabled",
"feature.notifications.service.telegram.enabled",
"feature.notifications.security_provider_events.enabled", // Blocker 3: Add security_provider_events gate
}
@@ -42,17 +43,13 @@ var defaultFlagValues = map[string]bool{
"feature.crowdsec.console_enrollment": false,
"feature.notifications.engine.notify_v1.enabled": false,
"feature.notifications.service.discord.enabled": false,
"feature.notifications.service.email.enabled": false,
"feature.notifications.service.gotify.enabled": false,
"feature.notifications.service.webhook.enabled": false,
"feature.notifications.legacy.fallback_enabled": false,
"feature.notifications.service.telegram.enabled": false,
"feature.notifications.security_provider_events.enabled": false, // Blocker 3: Default disabled for this stage
}
var retiredLegacyFallbackEnvAliases = []string{
"FEATURE_NOTIFICATIONS_LEGACY_FALLBACK_ENABLED",
"NOTIFICATIONS_LEGACY_FALLBACK_ENABLED",
}
// GetFlags returns a map of feature flag -> bool. DB setting takes precedence
// and falls back to environment variables if present.
func (h *FeatureFlagsHandler) GetFlags(c *gin.Context) {
@@ -86,11 +83,6 @@ func (h *FeatureFlagsHandler) GetFlags(c *gin.Context) {
defaultVal = v
}
if key == "feature.notifications.legacy.fallback_enabled" {
result[key] = h.resolveRetiredLegacyFallback(settingsMap)
continue
}
// Check if flag exists in DB
if s, exists := settingsMap[key]; exists {
v := strings.ToLower(strings.TrimSpace(s.Value))
@@ -131,40 +123,6 @@ func (h *FeatureFlagsHandler) GetFlags(c *gin.Context) {
c.JSON(http.StatusOK, result)
}
func parseFlagBool(raw string) (bool, bool) {
v := strings.ToLower(strings.TrimSpace(raw))
switch v {
case "1", "true", "yes":
return true, true
case "0", "false", "no":
return false, true
default:
return false, false
}
}
func (h *FeatureFlagsHandler) resolveRetiredLegacyFallback(settingsMap map[string]models.Setting) bool {
const retiredKey = "feature.notifications.legacy.fallback_enabled"
if s, exists := settingsMap[retiredKey]; exists {
if _, ok := parseFlagBool(s.Value); !ok {
log.Printf("[WARN] Invalid persisted retired fallback flag value, forcing disabled: key=%s value=%q", retiredKey, s.Value)
}
return false
}
for _, alias := range retiredLegacyFallbackEnvAliases {
if ev, ok := os.LookupEnv(alias); ok {
if _, parsed := parseFlagBool(ev); !parsed {
log.Printf("[WARN] Invalid environment retired fallback flag value, forcing disabled: key=%s value=%q", alias, ev)
}
return false
}
}
return false
}
// UpdateFlags accepts a JSON object map[string]bool and upserts settings.
func (h *FeatureFlagsHandler) UpdateFlags(c *gin.Context) {
// Phase 0: Performance instrumentation
@@ -180,14 +138,6 @@ func (h *FeatureFlagsHandler) UpdateFlags(c *gin.Context) {
return
}
if v, exists := payload["feature.notifications.legacy.fallback_enabled"]; exists && v {
c.JSON(http.StatusBadRequest, gin.H{
"error": "feature.notifications.legacy.fallback_enabled is retired and can only be false",
"code": "LEGACY_FALLBACK_REMOVED",
})
return
}
// Phase 1: Transaction wrapping - all updates in single atomic transaction
if err := h.DB.Transaction(func(tx *gorm.DB) error {
for k, v := range payload {
@@ -203,10 +153,6 @@ func (h *FeatureFlagsHandler) UpdateFlags(c *gin.Context) {
continue
}
if k == "feature.notifications.legacy.fallback_enabled" {
v = false
}
s := models.Setting{Key: k, Value: strconv.FormatBool(v), Type: "bool", Category: "feature"}
if err := tx.Where(models.Setting{Key: k}).Assign(s).FirstOrCreate(&s).Error; err != nil {
return err // Rollback on error

View File

@@ -460,3 +460,24 @@ func TestFeatureFlagsHandler_NewFeatureFlagsHandler(t *testing.T) {
assert.NotNil(t, h.DB)
assert.Equal(t, db, h.DB)
}
func TestFeatureFlagsHandler_GetFlags_EmailFlagDefaultFalse(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupFlagsDB(t)
h := NewFeatureFlagsHandler(db)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
var flags map[string]bool
err := json.Unmarshal(w.Body.Bytes(), &flags)
require.NoError(t, err)
assert.False(t, flags["feature.notifications.service.email.enabled"])
}

View File

@@ -100,147 +100,6 @@ func TestFeatureFlags_EnvFallback(t *testing.T) {
}
}
func TestFeatureFlags_RetiredFallback_DenyByDefault(t *testing.T) {
db := setupFlagsDB(t)
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 got %d body=%s", w.Code, w.Body.String())
}
var flags map[string]bool
if err := json.Unmarshal(w.Body.Bytes(), &flags); err != nil {
t.Fatalf("invalid json: %v", err)
}
if flags["feature.notifications.legacy.fallback_enabled"] {
t.Fatalf("expected retired fallback flag to be false by default")
}
}
func TestFeatureFlags_RetiredFallback_PersistedAndEnvStillResolveFalse(t *testing.T) {
db := setupFlagsDB(t)
if err := db.Create(&models.Setting{
Key: "feature.notifications.legacy.fallback_enabled",
Value: "true",
Type: "bool",
Category: "feature",
}).Error; err != nil {
t.Fatalf("failed to seed setting: %v", err)
}
t.Setenv("FEATURE_NOTIFICATIONS_LEGACY_FALLBACK_ENABLED", "true")
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 got %d body=%s", w.Code, w.Body.String())
}
var flags map[string]bool
if err := json.Unmarshal(w.Body.Bytes(), &flags); err != nil {
t.Fatalf("invalid json: %v", err)
}
if flags["feature.notifications.legacy.fallback_enabled"] {
t.Fatalf("expected retired fallback flag to remain false even when persisted/env are true")
}
}
func TestFeatureFlags_RetiredFallback_EnvAliasResolvesFalse(t *testing.T) {
db := setupFlagsDB(t)
t.Setenv("NOTIFICATIONS_LEGACY_FALLBACK_ENABLED", "true")
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 got %d body=%s", w.Code, w.Body.String())
}
var flags map[string]bool
if err := json.Unmarshal(w.Body.Bytes(), &flags); err != nil {
t.Fatalf("invalid json: %v", err)
}
if flags["feature.notifications.legacy.fallback_enabled"] {
t.Fatalf("expected retired fallback flag to remain false for env alias")
}
}
func TestFeatureFlags_UpdateRejectsLegacyFallbackTrue(t *testing.T) {
db := setupFlagsDB(t)
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.PUT("/api/v1/feature-flags", h.UpdateFlags)
payload := map[string]bool{
"feature.notifications.legacy.fallback_enabled": true,
}
b, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400 got %d body=%s", w.Code, w.Body.String())
}
}
func TestFeatureFlags_UpdatePersistsLegacyFallbackFalse(t *testing.T) {
db := setupFlagsDB(t)
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.PUT("/api/v1/feature-flags", h.UpdateFlags)
payload := map[string]bool{
"feature.notifications.legacy.fallback_enabled": false,
}
b, _ := json.Marshal(payload)
req := httptest.NewRequest(http.MethodPut, "/api/v1/feature-flags", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 got %d body=%s", w.Code, w.Body.String())
}
var s models.Setting
if err := db.Where("key = ?", "feature.notifications.legacy.fallback_enabled").First(&s).Error; err != nil {
t.Fatalf("expected setting persisted: %v", err)
}
if s.Value != "false" {
t.Fatalf("expected persisted fallback value false, got %s", s.Value)
}
}
// setupBenchmarkFlagsDB creates an in-memory SQLite database for feature flags benchmarks
func setupBenchmarkFlagsDB(b *testing.B) *gorm.DB {
b.Helper()
@@ -428,32 +287,3 @@ func TestUpdateFlags_TransactionAtomic(t *testing.T) {
t.Errorf("expected crowdsec.console_enrollment to be true, got %s", s3.Value)
}
}
// TestFeatureFlags_InvalidRetiredEnvAlias covers lines 157-158 (invalid env var warning)
func TestFeatureFlags_InvalidRetiredEnvAlias(t *testing.T) {
db := setupFlagsDB(t)
t.Setenv("NOTIFICATIONS_LEGACY_FALLBACK_ENABLED", "invalid-value")
h := NewFeatureFlagsHandler(db)
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/api/v1/feature-flags", h.GetFlags)
req := httptest.NewRequest(http.MethodGet, "/api/v1/feature-flags", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 got %d body=%s", w.Code, w.Body.String())
}
var flags map[string]bool
if err := json.Unmarshal(w.Body.Bytes(), &flags); err != nil {
t.Fatalf("invalid json: %v", err)
}
// Should force disabled due to invalid value (lines 157-158)
if flags["feature.notifications.legacy.fallback_enabled"] {
t.Fatalf("expected retired fallback flag to be false for invalid env value")
}
}

View File

@@ -50,7 +50,7 @@ func TestRemoteServerHandler_List(t *testing.T) {
}
db.Create(server)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))
@@ -74,7 +74,7 @@ func TestRemoteServerHandler_Create(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupTestDB(t)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))
@@ -119,7 +119,7 @@ func TestRemoteServerHandler_TestConnection(t *testing.T) {
}
db.Create(server)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))
@@ -154,7 +154,7 @@ func TestRemoteServerHandler_Get(t *testing.T) {
}
db.Create(server)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))
@@ -188,7 +188,7 @@ func TestRemoteServerHandler_Update(t *testing.T) {
}
db.Create(server)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))
@@ -234,7 +234,7 @@ func TestRemoteServerHandler_Delete(t *testing.T) {
}
db.Create(server)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))
@@ -271,7 +271,7 @@ func TestProxyHostHandler_List(t *testing.T) {
}
db.Create(host)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewProxyHostHandler(db, nil, ns, nil)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))
@@ -295,7 +295,7 @@ func TestProxyHostHandler_Create(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupTestDB(t)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewProxyHostHandler(db, nil, ns, nil)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))
@@ -343,7 +343,7 @@ func TestProxyHostHandler_PartialUpdate_DoesNotWipeFields(t *testing.T) {
}
db.Create(original)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewProxyHostHandler(db, nil, ns, nil)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))
@@ -408,7 +408,7 @@ func TestRemoteServerHandler_Errors(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupTestDB(t)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
router := gin.New()
handler.RegisterRoutes(router.Group("/api/v1"))

View File

@@ -2,6 +2,7 @@ package handlers
import (
"net/http"
"net/url"
"strings"
"time"
@@ -14,13 +15,24 @@ import (
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
// Allow all origins for development. In production, this should check
// against a whitelist of allowed origins.
return true
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
// No Origin header — non-browser client or same-origin request.
return true
}
originURL, err := url.Parse(origin)
if err != nil {
return false
}
requestHost := r.Host
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
requestHost = forwardedHost
}
return originURL.Host == requestHost
},
}
// LogEntry represents a structured log entry sent over WebSocket.

View File

@@ -33,6 +33,43 @@ func waitFor(t *testing.T, timeout time.Duration, condition func() bool) {
t.Fatalf("condition not met within %s", timeout)
}
func TestUpgraderCheckOrigin(t *testing.T) {
t.Parallel()
tests := []struct {
name string
origin string
host string
xForwardedHost string
want bool
}{
{"empty origin allows request", "", "example.com", "", true},
{"invalid URL origin rejects", "://bad-url", "example.com", "", false},
{"matching host allows", "http://example.com", "example.com", "", true},
{"non-matching host rejects", "http://evil.com", "example.com", "", false},
{"X-Forwarded-Host matching allows", "http://proxy.example.com", "backend.internal", "proxy.example.com", true},
{"X-Forwarded-Host non-matching rejects", "http://evil.com", "backend.internal", "proxy.example.com", false},
{"origin with port matching", "http://example.com:8080", "example.com:8080", "", true},
{"origin with port non-matching", "http://example.com:9090", "example.com:8080", "", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "/ws", http.NoBody)
if tc.origin != "" {
req.Header.Set("Origin", tc.origin)
}
req.Host = tc.host
if tc.xForwardedHost != "" {
req.Header.Set("X-Forwarded-Host", tc.xForwardedHost)
}
got := upgrader.CheckOrigin(req)
assert.Equal(t, tc.want, got, "origin=%q host=%q xfh=%q", tc.origin, tc.host, tc.xForwardedHost)
})
}
}
func TestLogsWebSocketHandler_DeprecatedWrapperUpgradeFailure(t *testing.T) {
gin.SetMode(gin.TestMode)
charonlogger.Init(false, io.Discard)

View File

@@ -35,7 +35,7 @@ func setAdminContext(c *gin.Context) {
func TestNotificationHandler_List_Error(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationHandler(svc)
// Drop the table to cause error
@@ -57,7 +57,7 @@ func TestNotificationHandler_List_Error(t *testing.T) {
func TestNotificationHandler_List_UnreadOnly(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationHandler(svc)
// Create some notifications
@@ -77,7 +77,7 @@ func TestNotificationHandler_List_UnreadOnly(t *testing.T) {
func TestNotificationHandler_MarkAsRead_Error(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationHandler(svc)
// Drop table to cause error
@@ -97,7 +97,7 @@ func TestNotificationHandler_MarkAsRead_Error(t *testing.T) {
func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationHandler(svc)
// Drop table to cause error
@@ -118,7 +118,7 @@ func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) {
func TestNotificationProviderHandler_List_Error(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
// Drop table to cause error
@@ -137,7 +137,7 @@ func TestNotificationProviderHandler_List_Error(t *testing.T) {
func TestNotificationProviderHandler_Create_InvalidJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
w := httptest.NewRecorder()
@@ -154,7 +154,7 @@ func TestNotificationProviderHandler_Create_InvalidJSON(t *testing.T) {
func TestNotificationProviderHandler_Create_DBError(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
// Drop table to cause error
@@ -182,7 +182,7 @@ func TestNotificationProviderHandler_Create_DBError(t *testing.T) {
func TestNotificationProviderHandler_Create_InvalidTemplate(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
provider := models.NotificationProvider{
@@ -208,7 +208,7 @@ func TestNotificationProviderHandler_Create_InvalidTemplate(t *testing.T) {
func TestNotificationProviderHandler_Update_InvalidJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
w := httptest.NewRecorder()
@@ -226,7 +226,7 @@ func TestNotificationProviderHandler_Update_InvalidJSON(t *testing.T) {
func TestNotificationProviderHandler_Update_InvalidTemplate(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
// Create a provider first
@@ -258,7 +258,7 @@ func TestNotificationProviderHandler_Update_InvalidTemplate(t *testing.T) {
func TestNotificationProviderHandler_Update_DBError(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
// Drop table to cause error
@@ -287,7 +287,7 @@ func TestNotificationProviderHandler_Update_DBError(t *testing.T) {
func TestNotificationProviderHandler_Delete_Error(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
// Drop table to cause error
@@ -307,7 +307,7 @@ func TestNotificationProviderHandler_Delete_Error(t *testing.T) {
func TestNotificationProviderHandler_Test_InvalidJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
w := httptest.NewRecorder()
@@ -324,7 +324,7 @@ func TestNotificationProviderHandler_Test_InvalidJSON(t *testing.T) {
func TestNotificationProviderHandler_Test_RejectsClientSuppliedGotifyToken(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
payload := map[string]any{
@@ -356,7 +356,7 @@ func TestNotificationProviderHandler_Test_RejectsClientSuppliedGotifyToken(t *te
func TestNotificationProviderHandler_Test_RejectsGotifyTokenWithWhitespace(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
payload := map[string]any{
@@ -474,10 +474,65 @@ func TestClassifyProviderTestFailure_TLSHandshakeFailed(t *testing.T) {
assert.Contains(t, message, "TLS handshake failed")
}
func TestClassifyProviderTestFailure_SlackInvalidPayload(t *testing.T) {
code, category, message := classifyProviderTestFailure(errors.New("invalid_payload"))
assert.Equal(t, "PROVIDER_TEST_VALIDATION_FAILED", code)
assert.Equal(t, "validation", category)
assert.Contains(t, message, "Slack rejected the payload")
}
func TestClassifyProviderTestFailure_SlackMissingTextOrFallback(t *testing.T) {
code, category, message := classifyProviderTestFailure(errors.New("missing_text_or_fallback"))
assert.Equal(t, "PROVIDER_TEST_VALIDATION_FAILED", code)
assert.Equal(t, "validation", category)
assert.Contains(t, message, "Slack rejected the payload")
}
func TestClassifyProviderTestFailure_SlackNoService(t *testing.T) {
code, category, message := classifyProviderTestFailure(errors.New("no_service"))
assert.Equal(t, "PROVIDER_TEST_AUTH_REJECTED", code)
assert.Equal(t, "dispatch", category)
assert.Contains(t, message, "Slack webhook is revoked")
}
func TestNotificationProviderHandler_Test_RejectsSlackTokenInTestRequest(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
payload := map[string]any{
"type": "slack",
"url": "#alerts",
"token": "https://hooks.slack.com/services/T00/B00/secret",
}
body, _ := json.Marshal(payload)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Set(string(trace.RequestIDKey), "req-slack-token-reject")
c.Request = httptest.NewRequest(http.MethodPost, "/providers/test", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
h.Test(c)
assert.Equal(t, http.StatusBadRequest, w.Code)
var resp map[string]any
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(t, "TOKEN_WRITE_ONLY", resp["code"])
assert.Equal(t, "validation", resp["category"])
assert.Equal(t, "Slack webhook URL is accepted only on provider create/update", resp["error"])
assert.NotContains(t, w.Body.String(), "hooks.slack.com")
}
func TestNotificationProviderHandler_Templates(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
w := httptest.NewRecorder()
@@ -495,7 +550,7 @@ func TestNotificationProviderHandler_Templates(t *testing.T) {
func TestNotificationProviderHandler_Preview_InvalidJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
w := httptest.NewRecorder()
@@ -512,7 +567,7 @@ func TestNotificationProviderHandler_Preview_InvalidJSON(t *testing.T) {
func TestNotificationProviderHandler_Preview_WithData(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
payload := map[string]any{
@@ -538,7 +593,7 @@ func TestNotificationProviderHandler_Preview_WithData(t *testing.T) {
func TestNotificationProviderHandler_Preview_InvalidTemplate(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
payload := map[string]any{
@@ -563,7 +618,7 @@ func TestNotificationProviderHandler_Preview_InvalidTemplate(t *testing.T) {
func TestNotificationTemplateHandler_List_Error(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
// Drop table to cause error
@@ -582,7 +637,7 @@ func TestNotificationTemplateHandler_List_Error(t *testing.T) {
func TestNotificationTemplateHandler_Create_BadJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
w := httptest.NewRecorder()
@@ -599,7 +654,7 @@ func TestNotificationTemplateHandler_Create_BadJSON(t *testing.T) {
func TestNotificationTemplateHandler_Create_DBError(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
// Drop table to cause error
@@ -625,7 +680,7 @@ func TestNotificationTemplateHandler_Create_DBError(t *testing.T) {
func TestNotificationTemplateHandler_Update_BadJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
w := httptest.NewRecorder()
@@ -643,7 +698,7 @@ func TestNotificationTemplateHandler_Update_BadJSON(t *testing.T) {
func TestNotificationTemplateHandler_Update_DBError(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
// Drop table to cause error
@@ -670,7 +725,7 @@ func TestNotificationTemplateHandler_Update_DBError(t *testing.T) {
func TestNotificationTemplateHandler_Delete_Error(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
// Drop table to cause error
@@ -690,7 +745,7 @@ func TestNotificationTemplateHandler_Delete_Error(t *testing.T) {
func TestNotificationTemplateHandler_Preview_BadJSON(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
w := httptest.NewRecorder()
@@ -707,7 +762,7 @@ func TestNotificationTemplateHandler_Preview_BadJSON(t *testing.T) {
func TestNotificationTemplateHandler_Preview_TemplateNotFound(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
payload := map[string]any{
@@ -730,7 +785,7 @@ func TestNotificationTemplateHandler_Preview_TemplateNotFound(t *testing.T) {
func TestNotificationTemplateHandler_Preview_WithStoredTemplate(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
// Create a template
@@ -762,7 +817,7 @@ func TestNotificationTemplateHandler_Preview_WithStoredTemplate(t *testing.T) {
func TestNotificationTemplateHandler_Preview_InvalidTemplate(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
payload := map[string]any{
@@ -784,7 +839,7 @@ func TestNotificationTemplateHandler_Preview_InvalidTemplate(t *testing.T) {
func TestNotificationProviderHandler_Preview_TokenWriteOnly(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
payload := map[string]any{
@@ -808,7 +863,7 @@ func TestNotificationProviderHandler_Preview_TokenWriteOnly(t *testing.T) {
func TestNotificationProviderHandler_Update_TypeChangeRejected(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
existing := models.NotificationProvider{
@@ -842,7 +897,7 @@ func TestNotificationProviderHandler_Update_TypeChangeRejected(t *testing.T) {
func TestNotificationProviderHandler_Test_MissingProviderID(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
payload := map[string]any{
@@ -865,7 +920,7 @@ func TestNotificationProviderHandler_Test_MissingProviderID(t *testing.T) {
func TestNotificationProviderHandler_Test_ProviderNotFound(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
payload := map[string]any{
@@ -889,7 +944,7 @@ func TestNotificationProviderHandler_Test_ProviderNotFound(t *testing.T) {
func TestNotificationProviderHandler_Test_EmptyProviderURL(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
existing := models.NotificationProvider{
@@ -942,20 +997,20 @@ func TestIsProviderValidationError_Comprehensive(t *testing.T) {
func TestNotificationProviderHandler_Update_UnsupportedType(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
existing := models.NotificationProvider{
ID: "unsupported-type",
Name: "Custom Provider",
Type: "slack",
URL: "https://hooks.slack.com/test",
Type: "sms",
URL: "https://sms.example.com/test",
}
require.NoError(t, db.Create(&existing).Error)
payload := map[string]any{
"name": "Updated Slack Provider",
"url": "https://hooks.slack.com/updated",
"name": "Updated SMS Provider",
"url": "https://sms.example.com/updated",
}
body, _ := json.Marshal(payload)
@@ -975,7 +1030,7 @@ func TestNotificationProviderHandler_Update_UnsupportedType(t *testing.T) {
func TestNotificationProviderHandler_Update_GotifyKeepsExistingToken(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
existing := models.NotificationProvider{
@@ -1013,7 +1068,7 @@ func TestNotificationProviderHandler_Update_GotifyKeepsExistingToken(t *testing.
func TestNotificationProviderHandler_Test_ReadDBError(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationCoverageDB(t)
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationProviderHandler(svc)
_ = db.Migrator().DropTable(&models.NotificationProvider{})

View File

@@ -36,7 +36,7 @@ func TestNotificationHandler_List(t *testing.T) {
db.Create(&models.Notification{Title: "Test 1", Message: "Msg 1", Read: false})
db.Create(&models.Notification{Title: "Test 2", Message: "Msg 2", Read: true})
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := handlers.NewNotificationHandler(service)
router := gin.New()
router.GET("/notifications", handler.List)
@@ -72,7 +72,7 @@ func TestNotificationHandler_MarkAsRead(t *testing.T) {
notif := &models.Notification{Title: "Test 1", Message: "Msg 1", Read: false}
db.Create(notif)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := handlers.NewNotificationHandler(service)
router := gin.New()
router.POST("/notifications/:id/read", handler.MarkAsRead)
@@ -96,7 +96,7 @@ func TestNotificationHandler_MarkAllAsRead(t *testing.T) {
db.Create(&models.Notification{Title: "Test 1", Message: "Msg 1", Read: false})
db.Create(&models.Notification{Title: "Test 2", Message: "Msg 2", Read: false})
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := handlers.NewNotificationHandler(service)
router := gin.New()
router.POST("/notifications/read-all", handler.MarkAllAsRead)
@@ -115,7 +115,7 @@ func TestNotificationHandler_MarkAllAsRead(t *testing.T) {
func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationTestDB(t)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := handlers.NewNotificationHandler(service)
r := gin.New()
@@ -134,7 +134,7 @@ func TestNotificationHandler_MarkAllAsRead_Error(t *testing.T) {
func TestNotificationHandler_DBError(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupNotificationTestDB(t)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := handlers.NewNotificationHandler(service)
r := gin.New()

View File

@@ -28,19 +28,22 @@ func TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents(t *testing.T
assert.NoError(t, err)
// Create handler
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil,
services.WithSlackURLValidator(func(string) error { return nil }),
)
handler := NewNotificationProviderHandler(service)
// Test cases: provider types with security events enabled
testCases := []struct {
name string
providerType string
token string
wantStatus int
}{
{"webhook", "webhook", http.StatusCreated},
{"gotify", "gotify", http.StatusCreated},
{"slack", "slack", http.StatusBadRequest},
{"email", "email", http.StatusBadRequest},
{"webhook", "webhook", "", http.StatusCreated},
{"gotify", "gotify", "", http.StatusCreated},
{"slack", "slack", "https://hooks.slack.com/services/T1234567890/B1234567890/XXXXXXXXXXXXXXXXXXXX", http.StatusCreated},
{"email", "email", "", http.StatusCreated},
}
for _, tc := range testCases {
@@ -50,6 +53,7 @@ func TestBlocker3_CreateProviderRejectsNonDiscordWithSecurityEvents(t *testing.T
"name": "Test Provider",
"type": tc.providerType,
"url": "https://example.com/webhook",
"token": tc.token,
"enabled": true,
"notify_security_waf_blocks": true, // Security event enabled
}
@@ -96,7 +100,7 @@ func TestBlocker3_CreateProviderAcceptsDiscordWithSecurityEvents(t *testing.T) {
assert.NoError(t, err)
// Create handler
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Create request payload with Discord provider and security events
@@ -144,7 +148,7 @@ func TestBlocker3_CreateProviderAcceptsNonDiscordWithoutSecurityEvents(t *testin
assert.NoError(t, err)
// Create handler
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Create request payload with webhook provider but no security events
@@ -200,7 +204,7 @@ func TestBlocker3_UpdateProviderRejectsNonDiscordWithSecurityEvents(t *testing.T
assert.NoError(t, db.Create(&existingProvider).Error)
// Create handler
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Try to update to enable security events (should be rejected)
@@ -256,7 +260,7 @@ func TestBlocker3_UpdateProviderAcceptsDiscordWithSecurityEvents(t *testing.T) {
assert.NoError(t, db.Create(&existingProvider).Error)
// Create handler
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Update to enable security events
@@ -302,7 +306,7 @@ func TestBlocker3_MultipleSecurityEventsEnforcesDiscordOnly(t *testing.T) {
assert.NoError(t, err)
// Create handler
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Test each security event field individually
@@ -359,7 +363,7 @@ func TestBlocker3_UpdateProvider_DatabaseError(t *testing.T) {
assert.NoError(t, err)
// Create handler
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Update payload

View File

@@ -24,21 +24,24 @@ func TestDiscordOnly_CreateRejectsNonDiscord(t *testing.T) {
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.Notification{}))
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil,
services.WithSlackURLValidator(func(string) error { return nil }),
)
handler := NewNotificationProviderHandler(service)
testCases := []struct {
name string
providerType string
token string
wantStatus int
wantCode string
}{
{"webhook", "webhook", http.StatusCreated, ""},
{"gotify", "gotify", http.StatusCreated, ""},
{"slack", "slack", http.StatusBadRequest, "UNSUPPORTED_PROVIDER_TYPE"},
{"telegram", "telegram", http.StatusBadRequest, "UNSUPPORTED_PROVIDER_TYPE"},
{"generic", "generic", http.StatusBadRequest, "UNSUPPORTED_PROVIDER_TYPE"},
{"email", "email", http.StatusBadRequest, "UNSUPPORTED_PROVIDER_TYPE"},
{"webhook", "webhook", "", http.StatusCreated, ""},
{"gotify", "gotify", "", http.StatusCreated, ""},
{"slack", "slack", "https://hooks.slack.com/services/T1234567890/B1234567890/XXXXXXXXXXXXXXXXXXXX", http.StatusCreated, ""},
{"telegram", "telegram", "", http.StatusCreated, ""},
{"generic", "generic", "", http.StatusBadRequest, "UNSUPPORTED_PROVIDER_TYPE"},
{"email", "email", "", http.StatusCreated, ""},
}
for _, tc := range testCases {
@@ -47,6 +50,7 @@ func TestDiscordOnly_CreateRejectsNonDiscord(t *testing.T) {
"name": "Test Provider",
"type": tc.providerType,
"url": "https://example.com/webhook",
"token": tc.token,
"enabled": true,
"notify_proxy_hosts": true,
}
@@ -83,7 +87,7 @@ func TestDiscordOnly_CreateAcceptsDiscord(t *testing.T) {
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.Notification{}))
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
payload := map[string]interface{}{
@@ -129,7 +133,7 @@ func TestDiscordOnly_UpdateRejectsTypeMutation(t *testing.T) {
}
require.NoError(t, db.Create(&deprecatedProvider).Error)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Try to change type to discord
@@ -183,7 +187,7 @@ func TestDiscordOnly_UpdateRejectsEnable(t *testing.T) {
}
require.NoError(t, db.Create(&deprecatedProvider).Error)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Try to enable the deprecated provider
@@ -231,7 +235,7 @@ func TestDiscordOnly_UpdateAllowsDisabledDeprecated(t *testing.T) {
}
require.NoError(t, db.Create(&deprecatedProvider).Error)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Update name (keeping type and enabled unchanged)
@@ -279,7 +283,7 @@ func TestDiscordOnly_UpdateAcceptsDiscord(t *testing.T) {
}
require.NoError(t, db.Create(&discordProvider).Error)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
// Update to enable security notifications
@@ -327,7 +331,7 @@ func TestDiscordOnly_DeleteAllowsDeprecated(t *testing.T) {
}
require.NoError(t, db.Create(&deprecatedProvider).Error)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
w := httptest.NewRecorder()
@@ -363,7 +367,7 @@ func TestDiscordOnly_ErrorCodes(t *testing.T) {
requestFunc: func(id string) (*http.Request, gin.Params) {
payload := map[string]interface{}{
"name": "Test",
"type": "slack",
"type": "sms",
"url": "https://example.com",
}
body, _ := json.Marshal(payload)
@@ -409,7 +413,7 @@ func TestDiscordOnly_ErrorCodes(t *testing.T) {
id := tc.setupFunc(db)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
req, params := tc.requestFunc(id)

View File

@@ -92,7 +92,7 @@ func respondSanitizedProviderError(c *gin.Context, status int, code, category, m
c.JSON(status, response)
}
var providerStatusCodePattern = regexp.MustCompile(`provider returned status\s+(\d{3})`)
var providerStatusCodePattern = regexp.MustCompile(`provider returned status\s+(\d{3})(?::\s*(.+))?`)
func classifyProviderTestFailure(err error) (code string, category string, message string) {
if err == nil {
@@ -107,14 +107,18 @@ func classifyProviderTestFailure(err error) (code string, category string, messa
return "PROVIDER_TEST_URL_INVALID", "validation", "Provider URL is invalid or blocked. Verify the URL and try again"
}
if statusMatch := providerStatusCodePattern.FindStringSubmatch(errText); len(statusMatch) == 2 {
if statusMatch := providerStatusCodePattern.FindStringSubmatch(errText); len(statusMatch) >= 2 {
hint := ""
if len(statusMatch) >= 3 && strings.TrimSpace(statusMatch[2]) != "" {
hint = ": " + strings.TrimSpace(statusMatch[2])
}
switch statusMatch[1] {
case "401", "403":
return "PROVIDER_TEST_AUTH_REJECTED", "dispatch", "Provider rejected authentication. Verify your Gotify token"
return "PROVIDER_TEST_AUTH_REJECTED", "dispatch", "Provider rejected authentication. Verify your credentials"
case "404":
return "PROVIDER_TEST_ENDPOINT_NOT_FOUND", "dispatch", "Provider endpoint was not found. Verify the provider URL path"
default:
return "PROVIDER_TEST_REMOTE_REJECTED", "dispatch", fmt.Sprintf("Provider rejected the test request (HTTP %s)", statusMatch[1])
return "PROVIDER_TEST_REMOTE_REJECTED", "dispatch", fmt.Sprintf("Provider rejected the test request (HTTP %s)%s", statusMatch[1], hint)
}
}
@@ -132,6 +136,16 @@ func classifyProviderTestFailure(err error) (code string, category string, messa
return "PROVIDER_TEST_UNREACHABLE", "dispatch", "Could not reach provider endpoint. Verify URL, DNS, and network connectivity"
}
if strings.Contains(errText, "invalid_payload") ||
strings.Contains(errText, "missing_text_or_fallback") {
return "PROVIDER_TEST_VALIDATION_FAILED", "validation",
"Slack rejected the payload. Ensure your template includes a 'text' or 'blocks' field"
}
if strings.Contains(errText, "no_service") {
return "PROVIDER_TEST_AUTH_REJECTED", "dispatch",
"Slack webhook is revoked or the app is disabled. Create a new webhook"
}
return "PROVIDER_TEST_FAILED", "dispatch", "Provider test failed"
}
@@ -168,7 +182,7 @@ func (h *NotificationProviderHandler) Create(c *gin.Context) {
}
providerType := strings.ToLower(strings.TrimSpace(req.Type))
if providerType != "discord" && providerType != "gotify" && providerType != "webhook" {
if providerType != "discord" && providerType != "gotify" && providerType != "webhook" && providerType != "email" && providerType != "telegram" && providerType != "slack" && providerType != "pushover" {
respondSanitizedProviderError(c, http.StatusBadRequest, "UNSUPPORTED_PROVIDER_TYPE", "validation", "Unsupported notification provider type")
return
}
@@ -228,12 +242,12 @@ func (h *NotificationProviderHandler) Update(c *gin.Context) {
}
providerType := strings.ToLower(strings.TrimSpace(existing.Type))
if providerType != "discord" && providerType != "gotify" && providerType != "webhook" {
if providerType != "discord" && providerType != "gotify" && providerType != "webhook" && providerType != "email" && providerType != "telegram" && providerType != "slack" && providerType != "pushover" {
respondSanitizedProviderError(c, http.StatusBadRequest, "UNSUPPORTED_PROVIDER_TYPE", "validation", "Unsupported notification provider type")
return
}
if providerType == "gotify" && strings.TrimSpace(req.Token) == "" {
if (providerType == "gotify" || providerType == "telegram" || providerType == "slack" || providerType == "pushover") && strings.TrimSpace(req.Token) == "" {
// Keep existing token if update payload omits token
req.Token = existing.Token
}
@@ -274,7 +288,8 @@ func isProviderValidationError(err error) bool {
strings.Contains(errMsg, "rendered template") ||
strings.Contains(errMsg, "failed to parse template") ||
strings.Contains(errMsg, "failed to render template") ||
strings.Contains(errMsg, "invalid Discord webhook URL")
strings.Contains(errMsg, "invalid Discord webhook URL") ||
strings.Contains(errMsg, "invalid Slack webhook URL")
}
func (h *NotificationProviderHandler) Delete(c *gin.Context) {
@@ -306,6 +321,38 @@ func (h *NotificationProviderHandler) Test(c *gin.Context) {
return
}
if providerType == "slack" && strings.TrimSpace(req.Token) != "" {
respondSanitizedProviderError(c, http.StatusBadRequest, "TOKEN_WRITE_ONLY", "validation", "Slack webhook URL is accepted only on provider create/update")
return
}
if providerType == "telegram" && strings.TrimSpace(req.Token) != "" {
respondSanitizedProviderError(c, http.StatusBadRequest, "TOKEN_WRITE_ONLY", "validation", "Telegram bot token is accepted only on provider create/update")
return
}
if providerType == "pushover" && strings.TrimSpace(req.Token) != "" {
respondSanitizedProviderError(c, http.StatusBadRequest, "TOKEN_WRITE_ONLY", "validation", "Pushover API token is accepted only on provider create/update")
return
}
// Email providers use global SMTP + recipients from the URL field; they don't require a saved provider ID.
if providerType == "email" {
provider := models.NotificationProvider{
ID: strings.TrimSpace(req.ID),
Name: req.Name,
Type: req.Type,
URL: req.URL,
}
if err := h.service.TestEmailProvider(provider); err != nil {
code, category, message := classifyProviderTestFailure(err)
respondSanitizedProviderError(c, http.StatusBadRequest, code, category, message)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Test notification sent"})
return
}
providerID := strings.TrimSpace(req.ID)
if providerID == "" {
respondSanitizedProviderError(c, http.StatusBadRequest, "MISSING_PROVIDER_ID", "validation", "Trusted provider ID is required for test dispatch")
@@ -322,7 +369,7 @@ func (h *NotificationProviderHandler) Test(c *gin.Context) {
return
}
if strings.TrimSpace(provider.URL) == "" {
if providerType != "slack" && strings.TrimSpace(provider.URL) == "" {
respondSanitizedProviderError(c, http.StatusBadRequest, "PROVIDER_CONFIG_MISSING", "validation", "Trusted provider configuration is incomplete")
return
}

View File

@@ -23,7 +23,7 @@ func setupNotificationProviderTest(t *testing.T) (*gin.Engine, *gorm.DB) {
db := handlers.OpenTestDB(t)
require.NoError(t, db.AutoMigrate(&models.NotificationProvider{}, &models.Notification{}))
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := handlers.NewNotificationProviderHandler(service)
r := gin.Default()
@@ -510,3 +510,193 @@ func TestNotificationProviderHandler_Create_ResponseHasHasToken(t *testing.T) {
assert.Equal(t, true, raw["has_token"])
assert.NotContains(t, w.Body.String(), "app-token-123")
}
func TestNotificationProviderHandler_Test_Email_NoMailService_Returns400(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
// mailService is nil in test setup — email test should return 400 (not MISSING_PROVIDER_ID)
payload := map[string]interface{}{
"type": "email",
"url": "user@example.com",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestNotificationProviderHandler_Test_Email_EmptyURL_Returns400(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
payload := map[string]interface{}{
"type": "email",
"url": "",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestNotificationProviderHandler_Test_Email_DoesNotRequireProviderID(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
// No ID field — email path must not return MISSING_PROVIDER_ID
payload := map[string]interface{}{
"type": "email",
"url": "user@example.com",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
var resp map[string]interface{}
_ = json.Unmarshal(w.Body.Bytes(), &resp)
assert.NotEqual(t, "MISSING_PROVIDER_ID", resp["code"])
}
func TestNotificationProviderHandler_Test_NonEmail_StillRequiresProviderID(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
payload := map[string]interface{}{
"type": "discord",
"url": "https://discord.com/api/webhooks/123/abc",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
var resp map[string]interface{}
_ = json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, "MISSING_PROVIDER_ID", resp["code"])
}
func TestNotificationProviderHandler_Create_Telegram(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
payload := map[string]interface{}{
"name": "My Telegram Bot",
"type": "telegram",
"url": "123456789",
"token": "bot123456789:ABCdefGHIjklMNOpqrSTUvwxYZ",
"template": "minimal",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var raw map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
assert.Equal(t, "telegram", raw["type"])
assert.Equal(t, true, raw["has_token"])
// Token must never appear in response
assert.NotContains(t, w.Body.String(), "bot123456789:ABCdefGHIjklMNOpqrSTUvwxYZ")
}
func TestNotificationProviderHandler_Update_TelegramTokenPreservation(t *testing.T) {
r, db := setupNotificationProviderTest(t)
p := models.NotificationProvider{
ID: "tg-preserve",
Name: "Telegram Bot",
Type: "telegram",
URL: "123456789",
Token: "original-bot-token",
}
require.NoError(t, db.Create(&p).Error)
// Update without token — token should be preserved
payload := map[string]interface{}{
"name": "Updated Telegram Bot",
"type": "telegram",
"url": "987654321",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("PUT", "/api/v1/notifications/providers/tg-preserve", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify token was preserved in DB
var dbProvider models.NotificationProvider
require.NoError(t, db.Where("id = ?", "tg-preserve").First(&dbProvider).Error)
assert.Equal(t, "original-bot-token", dbProvider.Token)
assert.Equal(t, "987654321", dbProvider.URL)
}
func TestNotificationProviderHandler_List_TelegramNeverExposesBotToken(t *testing.T) {
r, db := setupNotificationProviderTest(t)
p := models.NotificationProvider{
ID: "tg-secret",
Name: "Secret Telegram",
Type: "telegram",
URL: "123456789",
Token: "bot999:SECRETTOKEN",
}
require.NoError(t, db.Create(&p).Error)
req, _ := http.NewRequest("GET", "/api/v1/notifications/providers", http.NoBody)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.NotContains(t, w.Body.String(), "bot999:SECRETTOKEN")
assert.NotContains(t, w.Body.String(), "api.telegram.org")
var raw []map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &raw))
require.Len(t, raw, 1)
assert.Equal(t, true, raw[0]["has_token"])
_, hasTokenField := raw[0]["token"]
assert.False(t, hasTokenField, "raw token field must not appear in JSON response")
}
func TestNotificationProviderHandler_Test_TelegramTokenRejected(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
payload := map[string]any{
"type": "telegram",
"token": "bot123:TOKEN",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "TOKEN_WRITE_ONLY")
}
func TestNotificationProviderHandler_Test_PushoverTokenRejected(t *testing.T) {
r, _ := setupNotificationProviderTest(t)
payload := map[string]any{
"type": "pushover",
"token": "app-token-abc",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/v1/notifications/providers/test", bytes.NewBuffer(body))
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "TOKEN_WRITE_ONLY")
}

View File

@@ -33,7 +33,7 @@ func TestUpdate_BlockTypeMutationForNonDiscord(t *testing.T) {
}
require.NoError(t, db.Create(existing).Error)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
gin.SetMode(gin.TestMode)
@@ -85,7 +85,7 @@ func TestUpdate_AllowTypeMutationForDiscord(t *testing.T) {
}
require.NoError(t, db.Create(existing).Error)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
gin.SetMode(gin.TestMode)

View File

@@ -23,7 +23,7 @@ func TestNotificationTemplateHandler_CRUDAndPreview(t *testing.T) {
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationTemplate{}, &models.Notification{}, &models.NotificationProvider{}))
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
r := gin.New()
@@ -92,7 +92,7 @@ func TestNotificationTemplateHandler_Create_InvalidJSON(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file::memory:?mode=memory&cache=shared"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationTemplate{}))
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
r := gin.New()
r.Use(func(c *gin.Context) {
@@ -113,7 +113,7 @@ func TestNotificationTemplateHandler_Update_InvalidJSON(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file::memory:?mode=memory&cache=shared"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationTemplate{}))
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
r := gin.New()
r.Use(func(c *gin.Context) {
@@ -134,7 +134,7 @@ func TestNotificationTemplateHandler_Preview_InvalidJSON(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file::memory:?mode=memory&cache=shared"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationTemplate{}))
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
r := gin.New()
r.Use(func(c *gin.Context) {
@@ -155,7 +155,7 @@ func TestNotificationTemplateHandler_AdminRequired(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file::memory:?mode=memory&cache=shared"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationTemplate{}))
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
r := gin.New()
@@ -185,7 +185,7 @@ func TestNotificationTemplateHandler_List_DBError(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file::memory:?mode=memory&cache=shared"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationTemplate{}))
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
r := gin.New()
@@ -205,7 +205,7 @@ func TestNotificationTemplateHandler_WriteOps_DBError(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file::memory:?mode=memory&cache=shared"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationTemplate{}))
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
r := gin.New()
@@ -264,7 +264,7 @@ func TestNotificationTemplateHandler_WriteOps_PermissionErrorResponse(t *testing
_ = db.Callback().Delete().Remove(deleteHook)
})
svc := services.NewNotificationService(db)
svc := services.NewNotificationService(db, nil)
h := NewNotificationTemplateHandler(svc)
r := gin.New()

View File

@@ -236,10 +236,6 @@ func (h *ProxyHostHandler) resolveSecurityHeaderProfileReference(value any) (*ui
return nil, nil
}
if _, err := uuid.Parse(trimmed); err != nil {
return nil, parseErr
}
var profile models.SecurityHeaderProfile
if err := h.db.Select("id").Where("uuid = ?", trimmed).First(&profile).Error; err != nil {
if err == gorm.ErrRecordNotFound {
@@ -362,7 +358,7 @@ func (h *ProxyHostHandler) Create(c *gin.Context) {
if host.AdvancedConfig != "" {
var parsed any
if err := json.Unmarshal([]byte(host.AdvancedConfig), &parsed); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid advanced_config JSON: " + err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "advanced_config must be valid Caddy JSON (not Caddyfile syntax). See https://caddyserver.com/docs/json/ for the correct format."})
return
}
parsed = caddy.NormalizeAdvancedConfig(parsed)
@@ -404,7 +400,7 @@ func (h *ProxyHostHandler) Create(c *gin.Context) {
h.notificationService.SendExternal(c.Request.Context(),
"proxy_host",
"Proxy Host Created",
fmt.Sprintf("Proxy Host %s (%s) created", util.SanitizeForLog(host.Name), util.SanitizeForLog(host.DomainNames)),
"A new proxy host was successfully created.",
map[string]any{
"Name": util.SanitizeForLog(host.Name),
"Domains": util.SanitizeForLog(host.DomainNames),
@@ -590,7 +586,7 @@ func (h *ProxyHostHandler) Update(c *gin.Context) {
if v != "" && v != host.AdvancedConfig {
var parsed any
if err := json.Unmarshal([]byte(v), &parsed); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid advanced_config JSON: " + err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": "advanced_config must be valid Caddy JSON (not Caddyfile syntax). See https://caddyserver.com/docs/json/ for the correct format."})
return
}
parsed = caddy.NormalizeAdvancedConfig(parsed)
@@ -679,7 +675,7 @@ func (h *ProxyHostHandler) Delete(c *gin.Context) {
h.notificationService.SendExternal(c.Request.Context(),
"proxy_host",
"Proxy Host Deleted",
fmt.Sprintf("Proxy Host %s deleted", host.Name),
"A proxy host was successfully deleted.",
map[string]any{
"Name": host.Name,
"Action": "deleted",

View File

@@ -32,7 +32,7 @@ func setupTestRouterForSecurityHeaders(t *testing.T) (*gin.Engine, *gorm.DB) {
&models.NotificationProvider{},
))
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
h := NewProxyHostHandler(db, nil, ns, nil)
r := gin.New()
api := r.Group("/api/v1")

View File

@@ -36,7 +36,7 @@ func setupTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
&models.NotificationProvider{},
))
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
h := NewProxyHostHandler(db, nil, ns, nil)
r := gin.New()
api := r.Group("/api/v1")
@@ -60,7 +60,7 @@ func setupTestRouterWithReferenceTables(t *testing.T) (*gin.Engine, *gorm.DB) {
&models.NotificationProvider{},
))
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
h := NewProxyHostHandler(db, nil, ns, nil)
r := gin.New()
api := r.Group("/api/v1")
@@ -86,7 +86,7 @@ func setupTestRouterWithUptime(t *testing.T) (*gin.Engine, *gorm.DB) {
&models.Setting{},
))
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
us := services.NewUptimeService(db, ns)
h := NewProxyHostHandler(db, nil, ns, us)
r := gin.New()
@@ -100,7 +100,7 @@ func TestProxyHostHandler_ResolveAccessListReference_TargetedBranches(t *testing
t.Parallel()
_, db := setupTestRouterWithReferenceTables(t)
h := NewProxyHostHandler(db, nil, services.NewNotificationService(db), nil)
h := NewProxyHostHandler(db, nil, services.NewNotificationService(db, nil), nil)
resolved, err := h.resolveAccessListReference(true)
require.Error(t, err)
@@ -124,7 +124,7 @@ func TestProxyHostHandler_ResolveSecurityHeaderReference_TargetedBranches(t *tes
t.Parallel()
_, db := setupTestRouterWithReferenceTables(t)
h := NewProxyHostHandler(db, nil, services.NewNotificationService(db), nil)
h := NewProxyHostHandler(db, nil, services.NewNotificationService(db, nil), nil)
resolved, err := h.resolveSecurityHeaderProfileReference(" ")
require.NoError(t, err)
@@ -327,7 +327,7 @@ func TestProxyHostDelete_WithUptimeCleanup(t *testing.T) {
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}, &models.UptimeMonitor{}, &models.UptimeHeartbeat{}))
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
us := services.NewUptimeService(db, ns)
h := NewProxyHostHandler(db, nil, ns, us)
@@ -381,7 +381,7 @@ func TestProxyHostErrors(t *testing.T) {
manager := caddy.NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
// Setup Handler
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
h := NewProxyHostHandler(db, manager, ns, nil)
r := gin.New()
api := r.Group("/api/v1")
@@ -661,7 +661,7 @@ func TestProxyHostWithCaddyIntegration(t *testing.T) {
manager := caddy.NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
// Setup Handler
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
h := NewProxyHostHandler(db, manager, ns, nil)
r := gin.New()
api := r.Group("/api/v1")
@@ -1552,7 +1552,7 @@ func TestProxyHostUpdate_SecurityHeaderProfile_InvalidString(t *testing.T) {
var result map[string]any
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &result))
require.Contains(t, result["error"], "invalid security_header_profile_id")
require.Contains(t, result["error"], "security header profile not found")
}
// Test invalid float value (should fail gracefully)
@@ -1894,7 +1894,7 @@ func TestUpdate_IntegrationCaddyConfig(t *testing.T) {
client := caddy.NewClientWithExpectedPort(caddyServer.URL, expectedPortFromURL(t, caddyServer.URL))
manager := caddy.NewManager(client, db, tmpDir, "", false, config.SecurityConfig{})
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
h := NewProxyHostHandler(db, manager, ns, nil)
r := gin.New()
api := r.Group("/api/v1")

View File

@@ -36,7 +36,7 @@ func setupUpdateTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
&models.NotificationProvider{},
))
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
h := NewProxyHostHandler(db, nil, ns, nil)
r := gin.New()
@@ -732,7 +732,49 @@ func TestProxyHostUpdate_SecurityHeaderProfileID_InvalidString(t *testing.T) {
var result map[string]any
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &result))
assert.Contains(t, result["error"], "invalid security_header_profile_id")
assert.Contains(t, result["error"], "security header profile not found")
}
// TestProxyHostUpdate_SecurityHeaderProfileID_PresetSlugUUID tests that a preset-style UUID
// slug (e.g. "preset-basic") resolves correctly to the numeric profile ID via a DB lookup,
// bypassing the uuid.Parse gate that would otherwise reject non-standard slug formats.
func TestProxyHostUpdate_SecurityHeaderProfileID_PresetSlugUUID(t *testing.T) {
t.Parallel()
router, db := setupUpdateTestRouter(t)
// Create a profile whose UUID mimics a preset slug (non-standard UUID format)
slugUUID := "preset-basic"
profile := models.SecurityHeaderProfile{
UUID: slugUUID,
Name: "Basic Security",
IsPreset: true,
SecurityScore: 65,
}
require.NoError(t, db.Create(&profile).Error)
host := createTestProxyHost(t, db, "preset-slug-test")
updateBody := map[string]any{
"name": "Test Host Updated",
"domain_names": "preset-slug-test.test.com",
"forward_scheme": "http",
"forward_host": "localhost",
"forward_port": 8080,
"security_header_profile_id": slugUUID,
}
body, _ := json.Marshal(updateBody)
req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusOK, resp.Code)
var updated models.ProxyHost
require.NoError(t, db.First(&updated, "uuid = ?", host.UUID).Error)
require.NotNil(t, updated.SecurityHeaderProfileID)
assert.Equal(t, profile.ID, *updated.SecurityHeaderProfileID)
}
// TestProxyHostUpdate_SecurityHeaderProfileID_UnsupportedType tests that an unsupported type
@@ -820,6 +862,10 @@ func TestProxyHostUpdate_SecurityHeaderProfileID_ValidAssignment(t *testing.T) {
name: "as_string",
value: fmt.Sprintf("%d", profile.ID),
},
{
name: "as_uuid_string",
value: profile.UUID,
},
}
for _, tc := range testCases {
@@ -933,7 +979,7 @@ func TestBulkUpdateSecurityHeaders_DBError_NonNotFound(t *testing.T) {
}
require.NoError(t, db.Create(&host).Error)
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
h := NewProxyHostHandler(db, nil, ns, nil)
r := gin.New()

View File

@@ -73,7 +73,7 @@ func (h *RemoteServerHandler) Create(c *gin.Context) {
h.notificationService.SendExternal(c.Request.Context(),
"remote_server",
"Remote Server Added",
fmt.Sprintf("Remote Server %s (%s:%d) added", util.SanitizeForLog(server.Name), util.SanitizeForLog(server.Host), server.Port),
"A new remote server was successfully added.",
map[string]any{
"Name": util.SanitizeForLog(server.Name),
"Host": util.SanitizeForLog(server.Host),
@@ -142,7 +142,7 @@ func (h *RemoteServerHandler) Delete(c *gin.Context) {
h.notificationService.SendExternal(c.Request.Context(),
"remote_server",
"Remote Server Deleted",
fmt.Sprintf("Remote Server %s deleted", util.SanitizeForLog(server.Name)),
"A remote server was successfully deleted.",
map[string]any{
"Name": util.SanitizeForLog(server.Name),
"Action": "deleted",

View File

@@ -22,7 +22,7 @@ func setupRemoteServerTest_New(t *testing.T) (*gin.Engine, *handlers.RemoteServe
// Ensure RemoteServer table exists
_ = db.AutoMigrate(&models.RemoteServer{})
ns := services.NewNotificationService(db)
ns := services.NewNotificationService(db, nil)
handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns)
r := gin.Default()

View File

@@ -23,7 +23,7 @@ func TestSecurityEventIntakeCompileSuccess(t *testing.T) {
db := SetupCompatibilityTestDB(t)
// This test validates that the handler can be instantiated with all required dependencies
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
service := services.NewEnhancedSecurityNotificationService(db)
securityService := services.NewSecurityService(db)
managementCIDRs := []string{"127.0.0.0/8"}
@@ -47,7 +47,7 @@ func TestSecurityEventIntakeCompileSuccess(t *testing.T) {
func TestSecurityEventIntakeAuthLocalhost(t *testing.T) {
db := SetupCompatibilityTestDB(t)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
service := services.NewEnhancedSecurityNotificationService(db)
managementCIDRs := []string{"10.0.0.0/8"}
@@ -88,7 +88,7 @@ func TestSecurityEventIntakeAuthLocalhost(t *testing.T) {
func TestSecurityEventIntakeAuthManagementCIDR(t *testing.T) {
db := SetupCompatibilityTestDB(t)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
service := services.NewEnhancedSecurityNotificationService(db)
managementCIDRs := []string{"192.168.1.0/24", "10.0.0.0/8"}
@@ -129,7 +129,7 @@ func TestSecurityEventIntakeAuthManagementCIDR(t *testing.T) {
func TestSecurityEventIntakeAuthUnauthorizedIP(t *testing.T) {
db := SetupCompatibilityTestDB(t)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
service := services.NewEnhancedSecurityNotificationService(db)
managementCIDRs := []string{"192.168.1.0/24"}
@@ -175,7 +175,7 @@ func TestSecurityEventIntakeAuthUnauthorizedIP(t *testing.T) {
func TestSecurityEventIntakeAuthInvalidIP(t *testing.T) {
db := SetupCompatibilityTestDB(t)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
service := services.NewEnhancedSecurityNotificationService(db)
managementCIDRs := []string{"192.168.1.0/24"}
@@ -234,7 +234,7 @@ func TestSecurityEventIntakeDispatchInvoked(t *testing.T) {
}
require.NoError(t, db.Create(provider).Error)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
service := services.NewEnhancedSecurityNotificationService(db)
managementCIDRs := []string{"127.0.0.0/8"}
@@ -374,7 +374,7 @@ func TestSecurityEventIntakeDiscordOnly(t *testing.T) {
}
require.NoError(t, db.Create(webhookProvider).Error)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
service := services.NewEnhancedSecurityNotificationService(db)
managementCIDRs := []string{"127.0.0.0/8"}
@@ -419,7 +419,7 @@ func TestSecurityEventIntakeDiscordOnly(t *testing.T) {
func TestSecurityEventIntakeMalformedPayload(t *testing.T) {
db := SetupCompatibilityTestDB(t)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
service := services.NewEnhancedSecurityNotificationService(db)
managementCIDRs := []string{"127.0.0.0/8"}
@@ -454,7 +454,7 @@ func TestSecurityEventIntakeMalformedPayload(t *testing.T) {
func TestSecurityEventIntakeIPv6Localhost(t *testing.T) {
db := SetupCompatibilityTestDB(t)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
service := services.NewEnhancedSecurityNotificationService(db)
managementCIDRs := []string{"10.0.0.0/8"}

View File

@@ -224,7 +224,7 @@ func TestFinalBlocker3_SupportedProviderTypes_UnsupportedTypesIgnored(t *testing
db := SetupCompatibilityTestDB(t)
// Create ONLY unsupported providers
unsupportedTypes := []string{"telegram", "generic"}
unsupportedTypes := []string{"sms", "generic"}
for _, providerType := range unsupportedTypes {
provider := &models.NotificationProvider{

View File

@@ -238,7 +238,7 @@ func TestR6_LegacyWrite410GoneNoMutation(t *testing.T) {
func TestProviderCRUD_SecurityEventsIncludeCrowdSec(t *testing.T) {
db := setupSingleSourceTestDB(t)
service := services.NewNotificationService(db)
service := services.NewNotificationService(db, nil)
handler := NewNotificationProviderHandler(service)
gin.SetMode(gin.TestMode)

View File

@@ -40,7 +40,7 @@ func TestHandleSecurityEvent_TimestampZero(t *testing.T) {
enhancedService := services.NewEnhancedSecurityNotificationService(db)
securityService := services.NewSecurityService(db)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
h := NewSecurityNotificationHandlerWithDeps(enhancedService, securityService, "/tmp", notificationService, []string{"127.0.0.0/8"})
w := httptest.NewRecorder()
@@ -85,7 +85,7 @@ func TestHandleSecurityEvent_SendViaProvidersError(t *testing.T) {
assert.NoError(t, err)
securityService := services.NewSecurityService(db)
notificationService := services.NewNotificationService(db)
notificationService := services.NewNotificationService(db, nil)
mockService := &mockFailingService{}
h := NewSecurityNotificationHandlerWithDeps(mockService, securityService, "/tmp", notificationService, []string{"127.0.0.0/8"})

View File

@@ -1,681 +0,0 @@
package handlers
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/Wikid82/charon/backend/internal/services"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// mockSecurityNotificationService implements the service interface for controlled testing.
type mockSecurityNotificationService struct {
getSettingsFunc func() (*models.NotificationConfig, error)
updateSettingsFunc func(*models.NotificationConfig) error
}
func (m *mockSecurityNotificationService) GetSettings() (*models.NotificationConfig, error) {
if m.getSettingsFunc != nil {
return m.getSettingsFunc()
}
return &models.NotificationConfig{}, nil
}
func (m *mockSecurityNotificationService) UpdateSettings(c *models.NotificationConfig) error {
if m.updateSettingsFunc != nil {
return m.updateSettingsFunc(c)
}
return nil
}
func setupSecNotifTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&models.NotificationConfig{}))
return db
}
// TestNewSecurityNotificationHandler verifies constructor returns non-nil handler.
func TestNewSecurityNotificationHandler(t *testing.T) {
t.Parallel()
db := setupSecNotifTestDB(t)
svc := services.NewSecurityNotificationService(db)
handler := NewSecurityNotificationHandler(svc)
assert.NotNil(t, handler, "Handler should not be nil")
}
// TestSecurityNotificationHandler_GetSettings_Success tests successful settings retrieval.
func TestSecurityNotificationHandler_GetSettings_Success(t *testing.T) {
t.Parallel()
expectedConfig := &models.NotificationConfig{
ID: "test-id",
Enabled: true,
MinLogLevel: "warn",
WebhookURL: "https://example.com/webhook",
NotifyWAFBlocks: true,
NotifyACLDenies: false,
}
mockService := &mockSecurityNotificationService{
getSettingsFunc: func() (*models.NotificationConfig, error) {
return expectedConfig, nil
},
}
handler := NewSecurityNotificationHandler(mockService)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/api/v1/security/notifications/settings", http.NoBody)
handler.GetSettings(c)
assert.Equal(t, http.StatusOK, w.Code)
var config models.NotificationConfig
err := json.Unmarshal(w.Body.Bytes(), &config)
require.NoError(t, err)
assert.Equal(t, expectedConfig.ID, config.ID)
assert.Equal(t, expectedConfig.Enabled, config.Enabled)
assert.Equal(t, expectedConfig.MinLogLevel, config.MinLogLevel)
assert.Equal(t, expectedConfig.WebhookURL, config.WebhookURL)
assert.Equal(t, expectedConfig.NotifyWAFBlocks, config.NotifyWAFBlocks)
assert.Equal(t, expectedConfig.NotifyACLDenies, config.NotifyACLDenies)
}
// TestSecurityNotificationHandler_GetSettings_ServiceError tests service error handling.
func TestSecurityNotificationHandler_GetSettings_ServiceError(t *testing.T) {
t.Parallel()
mockService := &mockSecurityNotificationService{
getSettingsFunc: func() (*models.NotificationConfig, error) {
return nil, errors.New("database connection failed")
},
}
handler := NewSecurityNotificationHandler(mockService)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/api/v1/security/notifications/settings", http.NoBody)
handler.GetSettings(c)
assert.Equal(t, http.StatusInternalServerError, w.Code)
var response map[string]string
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response["error"], "Failed to retrieve settings")
}
// TestSecurityNotificationHandler_UpdateSettings_InvalidJSON tests malformed JSON handling.
func TestSecurityNotificationHandler_UpdateSettings_InvalidJSON(t *testing.T) {
t.Parallel()
mockService := &mockSecurityNotificationService{}
handler := NewSecurityNotificationHandler(mockService)
malformedJSON := []byte(`{enabled: true, "min_log_level": "error"`)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(malformedJSON))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateSettings(c)
assert.Equal(t, http.StatusBadRequest, w.Code)
var response map[string]string
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response["error"], "Invalid request body")
}
// TestSecurityNotificationHandler_UpdateSettings_InvalidMinLogLevel tests invalid log level rejection.
func TestSecurityNotificationHandler_UpdateSettings_InvalidMinLogLevel(t *testing.T) {
t.Parallel()
invalidLevels := []struct {
name string
level string
}{
{"trace", "trace"},
{"critical", "critical"},
{"fatal", "fatal"},
{"unknown", "unknown"},
}
for _, tc := range invalidLevels {
t.Run(tc.name, func(t *testing.T) {
mockService := &mockSecurityNotificationService{}
handler := NewSecurityNotificationHandler(mockService)
config := models.NotificationConfig{
Enabled: true,
MinLogLevel: tc.level,
NotifyWAFBlocks: true,
}
body, err := json.Marshal(config)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateSettings(c)
assert.Equal(t, http.StatusBadRequest, w.Code)
var response map[string]string
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response["error"], "Invalid min_log_level")
})
}
}
// TestSecurityNotificationHandler_UpdateSettings_InvalidWebhookURL_SSRF tests SSRF protection.
func TestSecurityNotificationHandler_UpdateSettings_InvalidWebhookURL_SSRF(t *testing.T) {
t.Parallel()
ssrfURLs := []struct {
name string
url string
}{
{"AWS Metadata", "http://169.254.169.254/latest/meta-data/"},
{"GCP Metadata", "http://metadata.google.internal/computeMetadata/v1/"},
{"Azure Metadata", "http://169.254.169.254/metadata/instance"},
{"Private IP 10.x", "http://10.0.0.1/admin"},
{"Private IP 172.16.x", "http://172.16.0.1/config"},
{"Private IP 192.168.x", "http://192.168.1.1/api"},
{"Link-local", "http://169.254.1.1/"},
}
for _, tc := range ssrfURLs {
t.Run(tc.name, func(t *testing.T) {
mockService := &mockSecurityNotificationService{}
handler := NewSecurityNotificationHandler(mockService)
config := models.NotificationConfig{
Enabled: true,
MinLogLevel: "error",
WebhookURL: tc.url,
NotifyWAFBlocks: true,
}
body, err := json.Marshal(config)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateSettings(c)
assert.Equal(t, http.StatusBadRequest, w.Code)
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response["error"], "Invalid webhook URL")
if help, ok := response["help"]; ok {
assert.Contains(t, help, "private networks")
}
})
}
}
// TestSecurityNotificationHandler_UpdateSettings_PrivateIPWebhook tests private IP handling.
func TestSecurityNotificationHandler_UpdateSettings_PrivateIPWebhook(t *testing.T) {
t.Parallel()
// Note: localhost is allowed by WithAllowLocalhost() option
localhostURLs := []string{
"http://127.0.0.1/hook",
"http://localhost/webhook",
"http://[::1]/api",
}
for _, url := range localhostURLs {
t.Run(url, func(t *testing.T) {
mockService := &mockSecurityNotificationService{
updateSettingsFunc: func(c *models.NotificationConfig) error {
return nil
},
}
handler := NewSecurityNotificationHandler(mockService)
config := models.NotificationConfig{
Enabled: true,
MinLogLevel: "warn",
WebhookURL: url,
}
body, err := json.Marshal(config)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateSettings(c)
// Localhost should be allowed with AllowLocalhost option
assert.Equal(t, http.StatusOK, w.Code, "Localhost should be allowed: %s", url)
})
}
}
// TestSecurityNotificationHandler_UpdateSettings_ServiceError tests database error handling.
func TestSecurityNotificationHandler_UpdateSettings_ServiceError(t *testing.T) {
t.Parallel()
mockService := &mockSecurityNotificationService{
updateSettingsFunc: func(c *models.NotificationConfig) error {
return errors.New("database write failed")
},
}
handler := NewSecurityNotificationHandler(mockService)
config := models.NotificationConfig{
Enabled: true,
MinLogLevel: "error",
WebhookURL: "http://localhost:9090/webhook", // Use localhost
NotifyWAFBlocks: true,
}
body, err := json.Marshal(config)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateSettings(c)
assert.Equal(t, http.StatusInternalServerError, w.Code)
var response map[string]string
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response["error"], "Failed to update settings")
}
// TestSecurityNotificationHandler_UpdateSettings_Success tests successful settings update.
func TestSecurityNotificationHandler_UpdateSettings_Success(t *testing.T) {
t.Parallel()
var capturedConfig *models.NotificationConfig
mockService := &mockSecurityNotificationService{
updateSettingsFunc: func(c *models.NotificationConfig) error {
capturedConfig = c
return nil
},
}
handler := NewSecurityNotificationHandler(mockService)
config := models.NotificationConfig{
Enabled: true,
MinLogLevel: "warn",
WebhookURL: "http://localhost:8080/security", // Use localhost which is allowed
NotifyWAFBlocks: true,
NotifyACLDenies: false,
}
body, err := json.Marshal(config)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateSettings(c)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]string
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, "Settings updated successfully", response["message"])
// Verify the service was called with the correct config
require.NotNil(t, capturedConfig)
assert.Equal(t, config.Enabled, capturedConfig.Enabled)
assert.Equal(t, config.MinLogLevel, capturedConfig.MinLogLevel)
assert.Equal(t, config.WebhookURL, capturedConfig.WebhookURL)
assert.Equal(t, config.NotifyWAFBlocks, capturedConfig.NotifyWAFBlocks)
assert.Equal(t, config.NotifyACLDenies, capturedConfig.NotifyACLDenies)
}
// TestSecurityNotificationHandler_UpdateSettings_EmptyWebhookURL tests empty webhook is valid.
func TestSecurityNotificationHandler_UpdateSettings_EmptyWebhookURL(t *testing.T) {
t.Parallel()
mockService := &mockSecurityNotificationService{
updateSettingsFunc: func(c *models.NotificationConfig) error {
return nil
},
}
handler := NewSecurityNotificationHandler(mockService)
config := models.NotificationConfig{
Enabled: true,
MinLogLevel: "info",
WebhookURL: "",
NotifyWAFBlocks: true,
NotifyACLDenies: true,
}
body, err := json.Marshal(config)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest("PUT", "/settings", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.UpdateSettings(c)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]string
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, "Settings updated successfully", response["message"])
}
func TestSecurityNotificationHandler_RouteAliasGet(t *testing.T) {
t.Parallel()
expectedConfig := &models.NotificationConfig{
ID: "alias-test-id",
Enabled: true,
MinLogLevel: "info",
WebhookURL: "https://example.com/webhook",
NotifyWAFBlocks: true,
NotifyACLDenies: true,
}
mockService := &mockSecurityNotificationService{
getSettingsFunc: func() (*models.NotificationConfig, error) {
return expectedConfig, nil
},
}
handler := NewSecurityNotificationHandler(mockService)
gin.SetMode(gin.TestMode)
router := gin.New()
router.GET("/api/v1/security/notifications/settings", handler.GetSettings)
router.GET("/api/v1/notifications/settings/security", handler.GetSettings)
originalWriter := httptest.NewRecorder()
originalRequest := httptest.NewRequest(http.MethodGet, "/api/v1/security/notifications/settings", http.NoBody)
router.ServeHTTP(originalWriter, originalRequest)
aliasWriter := httptest.NewRecorder()
aliasRequest := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/settings/security", http.NoBody)
router.ServeHTTP(aliasWriter, aliasRequest)
assert.Equal(t, http.StatusOK, originalWriter.Code)
assert.Equal(t, originalWriter.Code, aliasWriter.Code)
assert.Equal(t, originalWriter.Body.String(), aliasWriter.Body.String())
}
func TestSecurityNotificationHandler_RouteAliasUpdate(t *testing.T) {
t.Parallel()
legacyUpdates := 0
canonicalUpdates := 0
mockService := &mockSecurityNotificationService{
updateSettingsFunc: func(c *models.NotificationConfig) error {
if c.WebhookURL == "http://localhost:8080/security" {
canonicalUpdates++
}
return nil
},
}
handler := NewSecurityNotificationHandler(mockService)
config := models.NotificationConfig{
Enabled: true,
MinLogLevel: "warn",
WebhookURL: "http://localhost:8080/security",
NotifyWAFBlocks: true,
NotifyACLDenies: false,
}
body, err := json.Marshal(config)
require.NoError(t, err)
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(func(c *gin.Context) {
setAdminContext(c)
c.Next()
})
router.PUT("/api/v1/security/notifications/settings", handler.DeprecatedUpdateSettings)
router.PUT("/api/v1/notifications/settings/security", handler.UpdateSettings)
originalWriter := httptest.NewRecorder()
originalRequest := httptest.NewRequest(http.MethodPut, "/api/v1/security/notifications/settings", bytes.NewBuffer(body))
originalRequest.Header.Set("Content-Type", "application/json")
router.ServeHTTP(originalWriter, originalRequest)
aliasWriter := httptest.NewRecorder()
aliasRequest := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/settings/security", bytes.NewBuffer(body))
aliasRequest.Header.Set("Content-Type", "application/json")
router.ServeHTTP(aliasWriter, aliasRequest)
assert.Equal(t, http.StatusGone, originalWriter.Code)
assert.Equal(t, "true", originalWriter.Header().Get("X-Charon-Deprecated"))
assert.Equal(t, "/api/v1/notifications/settings/security", originalWriter.Header().Get("X-Charon-Canonical-Endpoint"))
assert.Equal(t, http.StatusOK, aliasWriter.Code)
assert.Equal(t, 0, legacyUpdates)
assert.Equal(t, 1, canonicalUpdates)
}
func TestSecurityNotificationHandler_DeprecatedRouteHeaders(t *testing.T) {
t.Parallel()
mockService := &mockSecurityNotificationService{
getSettingsFunc: func() (*models.NotificationConfig, error) {
return &models.NotificationConfig{Enabled: true, MinLogLevel: "warn"}, nil
},
updateSettingsFunc: func(c *models.NotificationConfig) error {
return nil
},
}
handler := NewSecurityNotificationHandler(mockService)
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(func(c *gin.Context) {
setAdminContext(c)
c.Next()
})
router.GET("/api/v1/security/notifications/settings", handler.DeprecatedGetSettings)
router.PUT("/api/v1/security/notifications/settings", handler.DeprecatedUpdateSettings)
router.GET("/api/v1/notifications/settings/security", handler.GetSettings)
router.PUT("/api/v1/notifications/settings/security", handler.UpdateSettings)
legacyGet := httptest.NewRecorder()
legacyGetReq := httptest.NewRequest(http.MethodGet, "/api/v1/security/notifications/settings", http.NoBody)
router.ServeHTTP(legacyGet, legacyGetReq)
require.Equal(t, http.StatusOK, legacyGet.Code)
assert.Equal(t, "true", legacyGet.Header().Get("X-Charon-Deprecated"))
assert.Equal(t, "/api/v1/notifications/settings/security", legacyGet.Header().Get("X-Charon-Canonical-Endpoint"))
canonicalGet := httptest.NewRecorder()
canonicalGetReq := httptest.NewRequest(http.MethodGet, "/api/v1/notifications/settings/security", http.NoBody)
router.ServeHTTP(canonicalGet, canonicalGetReq)
require.Equal(t, http.StatusOK, canonicalGet.Code)
assert.Empty(t, canonicalGet.Header().Get("X-Charon-Deprecated"))
body, err := json.Marshal(models.NotificationConfig{Enabled: true, MinLogLevel: "warn"})
require.NoError(t, err)
legacyPut := httptest.NewRecorder()
legacyPutReq := httptest.NewRequest(http.MethodPut, "/api/v1/security/notifications/settings", bytes.NewBuffer(body))
legacyPutReq.Header.Set("Content-Type", "application/json")
router.ServeHTTP(legacyPut, legacyPutReq)
require.Equal(t, http.StatusGone, legacyPut.Code)
assert.Equal(t, "true", legacyPut.Header().Get("X-Charon-Deprecated"))
assert.Equal(t, "/api/v1/notifications/settings/security", legacyPut.Header().Get("X-Charon-Canonical-Endpoint"))
var legacyBody map[string]string
err = json.Unmarshal(legacyPut.Body.Bytes(), &legacyBody)
require.NoError(t, err)
assert.Len(t, legacyBody, 2)
assert.Equal(t, "This endpoint is deprecated and no longer accepts updates", legacyBody["error"])
assert.Equal(t, "/api/v1/notifications/settings/security", legacyBody["canonical_endpoint"])
canonicalPut := httptest.NewRecorder()
canonicalPutReq := httptest.NewRequest(http.MethodPut, "/api/v1/notifications/settings/security", bytes.NewBuffer(body))
canonicalPutReq.Header.Set("Content-Type", "application/json")
router.ServeHTTP(canonicalPut, canonicalPutReq)
require.Equal(t, http.StatusOK, canonicalPut.Code)
}
func TestNormalizeEmailRecipients(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr string
}{
{
name: "empty input",
input: " ",
want: "",
},
{
name: "single valid",
input: "admin@example.com",
want: "admin@example.com",
},
{
name: "multiple valid with spaces and blanks",
input: " admin@example.com, , ops@example.com ,security@example.com ",
want: "admin@example.com, ops@example.com, security@example.com",
},
{
name: "duplicates and mixed case preserved",
input: "Admin@Example.com, admin@example.com, Admin@Example.com",
want: "Admin@Example.com, admin@example.com, Admin@Example.com",
},
{
name: "invalid only",
input: "not-an-email",
wantErr: "invalid email recipients: not-an-email",
},
{
name: "mixed invalid and valid",
input: "admin@example.com, bad-address,ops@example.com",
wantErr: "invalid email recipients: bad-address",
},
{
name: "multiple invalids",
input: "bad-address,also-bad",
wantErr: "invalid email recipients: bad-address, also-bad",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := normalizeEmailRecipients(tt.input)
if tt.wantErr != "" {
require.Error(t, err)
assert.Equal(t, tt.wantErr, err.Error())
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
// TestSecurityNotificationHandler_DeprecatedUpdateSettings_AllFields tests that all JSON fields are returned
func TestSecurityNotificationHandler_DeprecatedUpdateSettings_AllFields(t *testing.T) {
t.Parallel()
mockService := &mockSecurityNotificationService{}
handler := NewSecurityNotificationHandler(mockService)
body, err := json.Marshal(models.NotificationConfig{Enabled: true, MinLogLevel: "warn"})
require.NoError(t, err)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
setAdminContext(c)
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/security/notifications/settings", bytes.NewBuffer(body))
c.Request.Header.Set("Content-Type", "application/json")
handler.DeprecatedUpdateSettings(c)
assert.Equal(t, http.StatusGone, w.Code)
assert.Equal(t, "true", w.Header().Get("X-Charon-Deprecated"))
assert.Equal(t, "/api/v1/notifications/settings/security", w.Header().Get("X-Charon-Canonical-Endpoint"))
var response map[string]string
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Verify both JSON fields are present with exact values
assert.Equal(t, "This endpoint is deprecated and no longer accepts updates", response["error"])
assert.Equal(t, "/api/v1/notifications/settings/security", response["canonical_endpoint"])
assert.Len(t, response, 2, "Should have exactly 2 fields in JSON response")
}

View File

@@ -114,7 +114,7 @@ func isSensitiveSettingKey(key string) bool {
type UpdateSettingRequest struct {
Key string `json:"key" binding:"required"`
Value string `json:"value" binding:"required"`
Value string `json:"value"`
Category string `json:"category"`
Type string `json:"type"`
}
@@ -131,16 +131,6 @@ func (h *SettingsHandler) UpdateSetting(c *gin.Context) {
return
}
// Block legacy fallback flag writes (LEGACY_FALLBACK_REMOVED)
if req.Key == "feature.notifications.legacy.fallback_enabled" &&
strings.EqualFold(strings.TrimSpace(req.Value), "true") {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Legacy fallback has been removed and cannot be re-enabled",
"code": "LEGACY_FALLBACK_REMOVED",
})
return
}
if req.Key == "security.admin_whitelist" {
if err := validateAdminWhitelist(req.Value); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid admin_whitelist: %v", err)})
@@ -279,12 +269,6 @@ func (h *SettingsHandler) PatchConfig(c *gin.Context) {
if err := h.DB.Transaction(func(tx *gorm.DB) error {
for key, value := range updates {
// Block legacy fallback flag writes (LEGACY_FALLBACK_REMOVED)
if key == "feature.notifications.legacy.fallback_enabled" &&
strings.EqualFold(strings.TrimSpace(value), "true") {
return fmt.Errorf("legacy fallback has been removed and cannot be re-enabled")
}
if key == "security.admin_whitelist" {
if err := validateAdminWhitelist(value); err != nil {
return fmt.Errorf("invalid admin_whitelist: %w", err)
@@ -321,13 +305,6 @@ func (h *SettingsHandler) PatchConfig(c *gin.Context) {
return nil
}); err != nil {
if strings.Contains(err.Error(), "legacy fallback has been removed") {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Legacy fallback has been removed and cannot be re-enabled",
"code": "LEGACY_FALLBACK_REMOVED",
})
return
}
if errors.Is(err, services.ErrInvalidAdminCIDR) || strings.Contains(err.Error(), "invalid admin_whitelist") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid admin_whitelist"})
return
@@ -657,7 +634,10 @@ func (h *SettingsHandler) SendTestEmail(c *gin.Context) {
</html>
`
if err := h.MailService.SendEmail(req.To, "Charon - Test Email", htmlBody); err != nil {
// req.To is validated as RFC 5321 email via gin binding:"required,email".
// SendEmail enforces validateEmailRecipients + net/mail.ParseAddress + rejectCRLF as defence-in-depth.
// Suppression annotations are on the SMTP sinks in mail_service.go.
if err := h.MailService.SendEmail(c.Request.Context(), []string{req.To}, "Charon - Test Email", htmlBody); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": err.Error(),

Some files were not shown because too many files have changed in this diff Show More