Repository navigation
test(security): add cross-cutting regression suite #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
gnanirahulnutakki
wants to merge
17
commits into
main
Choose a base branch
from
gnanirahulnutakki/issue-28-security-regression-suite
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
79d3b00
test(security): add cross-cutting regression suite
301d942
test(security): protect failed golden updates
6d0e0ca
test(security): close regression coverage gaps
d4e1f17
test(security): enforce retained rejection contracts
aea4de1
test(security): preserve failure diagnostics
232cda0
test(security): separate credential canaries
b47bdab
test(security): close boundary review gaps
11adb70
test(security): harden boundary response oracle
c22bd06
test(security): close boundary review gaps
9c450b5
test(security): bind operation and auth evidence
fac622a
test(security): harden response and auth ordering
b840355
test(security): scan successful responses for outsider data
65232c7
test(security): close response oracle bypasses
f353057
test(security): prove denied mutation integrity
64f1742
test(security): bind state and response contracts
2284345
test(security): close mutation and response regression gaps
2cb5826
test(security): verify rejected mutations preserve reusable keys
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # Security regression suite | ||
|
|
||
| Status: executable evidence for issue #28 at the current `main` contract. This | ||
| report covers repository components that exist. It does not qualify future | ||
| production storage, queue, worker, provider-adapter, telemetry-export, or | ||
| support-bundle implementations. | ||
|
|
||
| ## Public-route security matrix | ||
|
|
||
| [`TestPublicRouteSecurityMatrix`](../../test/security/security_regression_test.go) | ||
| derives the complete operation set from the validated OpenAPI artifact. The | ||
| test fails if a public operation is added without a matrix row. Its checked-in | ||
| result is | ||
| [`security-regression-v1alpha1.golden.json`](../../test/security/testdata/security-regression-v1alpha1.golden.json). | ||
|
|
||
| | Operation | Authenticated member | Authenticated outsider | Missing or invalid credential | | ||
| | --- | --- | --- | --- | | ||
| | `listWorkspaces` | `200`, authorized row returned | `200`, denied rows removed before pagination | `401` | | ||
| | `createWorkspace` | `403`, service-reserved tenant action | `403`, service-reserved tenant action | `401` | | ||
| | `getWorkspace` | `200` | `403` | `401` | | ||
| | `replaceWorkspace` | `202` | `403` | `401` | | ||
| | `deleteWorkspace` | Authorization passes; `409` protects the retained Policy child | `403` | `401` | | ||
| | `replaceWorkspaceStatus` | `403`, service-reserved tenant action | `403`, service-reserved tenant action | `401` | | ||
| | `getOperation` | `200` | `403` | `401` | | ||
|
|
||
| Workspace creation and status replacement intentionally have no tenant allow | ||
| case. The closed authorization matrix reserves both actions. Treating a `2xx` | ||
| response as required evidence would weaken the security contract. Their | ||
| positive path proves that a valid credential reaches the authorization layer; | ||
| their invariant is a tenant `403` for every role. | ||
|
|
||
| ## Retained untrusted-input and fuzz corpus | ||
|
|
||
| The following fuzz targets run their seed corpus during ordinary `go test` and | ||
| remain available for time-bounded fuzzing: | ||
|
|
||
| | Boundary | Target | Retained input classes | | ||
| | --- | --- | --- | | ||
| | Public HTTP routes | `FuzzReferencePublicBoundary` | alternate token carrier, encoded path, duplicate JSON member, unknown credential-like field, trailing JSON, invalid UTF-8, depth bomb, webhook/remote-reference shape, oversized body, invalid credential | | ||
| | Bearer extraction | `FuzzExtractBearer`, `FuzzCredentialTrailerScrubbing` | header/query/cookie/trailer ambiguity, divergent query views, body and trailer aliasing, arbitrary casing | | ||
| | OIDC/JWKS | deterministic negative corpus in `internal/adapters/identity/oidc` | non-canonical JWT segments, duplicate members, forbidden remote key headers, claim failures, redirect/cookie response state, malformed and over-bound keys | | ||
| | Resource admission | `FuzzAdmitRaw` | every resource kind, duplicate and unknown fields, invalid Unicode, depth/node/byte bounds, trailing values | | ||
| | Closed domain values | fuzz targets under `internal/core/domain` | invalid sum types, identifiers, hierarchy, role/action vocabulary, operations, conditions, audit canonicalization, credentials, and reconciliation digests | | ||
|
|
||
| The public-boundary target asserts panic freedom, bounded JSON responses, | ||
| mandatory no-store/nosniff/correlation headers, and bearer-canary absence. The | ||
| OpenAPI validator independently rejects remote servers, remote references, and | ||
| webhook expansion. No provider response parser exists yet; provider-specific | ||
| malicious-response corpora belong with the adapters introduced by issues #38 | ||
| and #41. | ||
|
|
||
| Run the public-boundary fuzzer for one minute with: | ||
|
|
||
| ```sh | ||
| go test ./test/security -run '^$' -fuzz '^FuzzReferencePublicBoundary$' -fuzztime=1m | ||
| ``` | ||
|
|
||
| ## Redaction report | ||
|
|
||
| | Surface named by issue #28 | Implemented evidence | Result | | ||
| | --- | --- | --- | | ||
| | Plans and reconciliation state | `TestOpaqueValuesRejectGenericSerializationAndRedactDiagnostics` covers plans, evidence, effects, attempts, leases, queue/idempotency state, proofs, and `slog` output | Secret and identity canaries absent; generic serialization rejected | | ||
| | Credential state | `TestCredentialValuesRedactAndRejectSerialization`, `FuzzSourceMaterialSafety`, and broker lease/lifecycle tests cover source/session material, requests, brokers, leases, rotation, errors, and destruction | Raw material remains callback-bounded; diagnostics redact; serialization rejected | | ||
| | Identity and bearer state | Identity diagnostic/serialization tests, bearer canary tests, OIDC negative corpus, and the route matrix cover principals, request carriers, challenges, problems, and headers | Raw token and identity claims absent from output; rejected request carriers scrubbed | | ||
| | Audit and privileged state | `TestOpaqueRuntimeValuesForbidLossyGenericSerialization` and `TestAdministrationDiagnosticsAndSerializationAreSafe` cover audit/elevation values, nested containers, errors, formatting, and `slog` | Canaries absent; opaque runtime values reject lossy serialization | | ||
| | HTTP errors | Route matrix and `FuzzReferencePublicBoundary` exercise every public route plus malformed/bounded input | Only closed problem codes and bounded field paths are returned; bearer canaries absent | | ||
| | Logs | Package redaction tests send every sensitive value through `fmt` and `slog` | Canaries absent from implemented logging surfaces | | ||
| | Traces | No tracer or trace exporter exists | Not applicable at this revision; issue #63 must add canaries with the first implementation | | ||
| | Metrics | No metrics registry or exporter exists | Not applicable at this revision; issue #63 must add label/value canaries and cardinality bounds | | ||
| | Support output | No support-bundle or diagnostic export exists | Not applicable at this revision; the implementing issue must add a closed allowlist and canary corpus | | ||
|
|
||
| This report proves current executable behavior only. A future component does | ||
| not inherit an “issue #28 passed” claim: its pull request must extend this | ||
| matrix, fuzz corpus, and redaction table before merge. | ||
|
|
||
| ## Verification | ||
|
|
||
| ```sh | ||
| go test ./test/security -count=1 | ||
| go test ./internal/transport/http ./internal/adapters/identity/oidc \ | ||
| ./internal/core/domain/admission ./internal/core/domain/credential \ | ||
| ./internal/core/service/credentialbroker ./internal/core/domain/reconciliation \ | ||
| ./internal/core/domain/audit ./internal/core/domain/administration -count=1 | ||
| ./hack/dev security | ||
| ./hack/dev check | ||
| ./hack/dev race | ||
| ``` | ||
|
|
||
| The OAuth token restrictions are cross-checked against | ||
| [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700.html), and the retained | ||
| regression practice follows the verification intent of | ||
| [NIST SP 800-218 SSDF](https://csrc.nist.gov/pubs/sp/800/218/final). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| package security_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/ArdurAI/veer/internal/core/domain/resource" | ||
| "github.com/ArdurAI/veer/internal/core/service/reference" | ||
| ) | ||
|
|
||
| func FuzzReferencePublicBoundary(f *testing.F) { | ||
| seeds := []struct { | ||
| method, target, authorization, contentType, idempotencyKey, ifMatch, requestID string | ||
| body []byte | ||
| }{ | ||
| {http.MethodGet, "/api/v1alpha1/workspaces", "Bearer " + securityBearerCanary, "", "", "", "req-fuzz-list", nil}, | ||
| {http.MethodGet, "/api/v1alpha1/workspaces?access_token=" + invalidBearerCanary, "Bearer " + securityBearerCanary, "", "", "", "req-fuzz-query-token", nil}, | ||
| {http.MethodGet, "/api/v1alpha1/workspaces/%2e%2e", "Bearer " + securityBearerCanary, "", "", "", "req-fuzz-encoded-path", nil}, | ||
| {http.MethodPost, "/api/v1alpha1/workspaces", "Bearer " + securityBearerCanary, "application/json", "fuzz-duplicate-0001", "", "req-fuzz-duplicate", []byte(`{"apiVersion":"v1alpha1","apiVersion":"v2","kind":"Workspace","metadata":{"displayName":"x"},"spec":{}}`)}, | ||
| {http.MethodPost, "/api/v1alpha1/workspaces", "Bearer " + securityBearerCanary, "application/json", "fuzz-unknown-0001", "", "req-fuzz-unknown", []byte(`{"apiVersion":"v1alpha1","kind":"Workspace","metadata":{"displayName":"x"},"spec":{"credential":"` + invalidBearerCanary + `"}}`)}, | ||
| {http.MethodPost, "/api/v1alpha1/workspaces", "Bearer " + securityBearerCanary, "application/json", "fuzz-trailing-0001", "", "req-fuzz-trailing", []byte(`{} {}`)}, | ||
| {http.MethodPost, "/api/v1alpha1/workspaces", "Bearer " + securityBearerCanary, "application/json", "fuzz-unicode-0001", "", "req-fuzz-unicode", []byte{'{', '"', 0xff, '"', ':', '0', '}'}}, | ||
| {http.MethodPost, "/api/v1alpha1/workspaces", "Bearer " + securityBearerCanary, "application/json", "fuzz-depth-0000001", "", "req-fuzz-depth", []byte(strings.Repeat("[", 65) + "0" + strings.Repeat("]", 65))}, | ||
| {http.MethodPost, "/api/v1alpha1/workspaces", "Bearer " + securityBearerCanary, "application/json", "fuzz-webhook-0001", "", "req-fuzz-webhook", []byte(`{"webhooks":{"escape":{"post":{"requestBody":{"$ref":"https://attacker.invalid/schema"}}}}}`)}, | ||
| {http.MethodPut, "/api/v1alpha1/workspaces/wsp_0000000000000001/status", "Bearer " + securityBearerCanary, "application/json", "fuzz-status-00001", `"rv_0000000000000001"`, "req-fuzz-status", bytes.Repeat([]byte{'x'}, resource.MaxCanonicalBytes+1)}, | ||
| {http.MethodDelete, "/api/v1alpha1/workspaces/wsp_0000000000000001", "Bearer " + invalidBearerCanary, "", "fuzz-delete-00001", `"rv_0000000000000001"`, "req-fuzz-delete", nil}, | ||
| } | ||
| for _, seed := range seeds { | ||
| f.Add(seed.method, seed.target, seed.authorization, seed.contentType, seed.idempotencyKey, seed.ifMatch, seed.requestID, seed.body) | ||
| } | ||
|
|
||
| f.Fuzz(func( | ||
| t *testing.T, | ||
| method, target, authorization, contentType, idempotencyKey, ifMatch, requestID string, | ||
| body []byte, | ||
| ) { | ||
| if len(method) > 32 || len(target) > 4*1024 || len(authorization) > 16*1024 || | ||
| len(contentType) > 512 || len(idempotencyKey) > 2*1024 || len(ifMatch) > 512 || | ||
| len(requestID) > 512 || len(body) > 2*resource.MaxCanonicalBytes { | ||
| t.Skip() | ||
| } | ||
| fixture := newSecurityFixture(t) | ||
| request, err := http.NewRequestWithContext(context.Background(), method, target, bytes.NewReader(body)) | ||
| if err != nil { | ||
| return | ||
| } | ||
| request.RequestURI = target | ||
| for name, value := range map[string]string{ | ||
| "Authorization": authorization, | ||
| "Content-Type": contentType, | ||
| "Idempotency-Key": idempotencyKey, | ||
| "If-Match": ifMatch, | ||
| "Veer-Request-Id": requestID, | ||
| } { | ||
| if value != "" { | ||
| request.Header.Set(name, value) | ||
| } | ||
| } | ||
| response := httptest.NewRecorder() | ||
| fixture.memberHandler.ServeHTTP(response, request) | ||
|
|
||
| if response.Code < 100 || response.Code > 599 { | ||
| t.Fatalf("invalid HTTP status %d", response.Code) | ||
| } | ||
| if response.Header().Get("Cache-Control") != "no-store" || | ||
| response.Header().Get("X-Content-Type-Options") != "nosniff" || | ||
| response.Header().Get("Veer-Request-Id") == "" { | ||
| t.Fatalf("security headers = %#v", response.Header()) | ||
|
gnanirahulnutakki marked this conversation as resolved.
Resolved
|
||
| } | ||
| if response.Body.Len() > reference.MaxPageBytes || !json.Valid(response.Body.Bytes()) { | ||
| t.Fatalf("response is unbounded or invalid JSON: bytes=%d body=%q", response.Body.Len(), response.Body.Bytes()) | ||
|
gnanirahulnutakki marked this conversation as resolved.
Resolved
|
||
| } | ||
| assertNoBearerCanary(t, response) | ||
|
gnanirahulnutakki marked this conversation as resolved.
Resolved
|
||
| }) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.