Files
siriusscan--sirius/testing/security/suite_api.go
T
wehub-resource-sync 161ef94b4f
Check engine pin consistency / Dockerfile / CI pin consistency (push) Successful in 8s
Sirius CI/CD Pipeline / Detect Changes (push) Successful in 23s
Validate Docker Configuration / Validate Docker Compose Configuration (push) Successful in 47s
Sirius CI/CD Pipeline / Build API (${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Build UI (${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Engine Manifest (push) Has been cancelled
Sirius CI/CD Pipeline / Merge API Manifest (push) Has been cancelled
Sirius CI/CD Pipeline / Merge UI Manifest (push) Has been cancelled
Sirius CI/CD Pipeline / Build Engine (${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Build Infra (${{ matrix.service }}, ${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Infra Manifest (sirius-postgres) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Infra Manifest (sirius-rabbitmq) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Infra Manifest (sirius-valkey) (push) Has been cancelled
Sirius CI/CD Pipeline / Integration Test (push) Has been cancelled
Sirius CI/CD Pipeline / Public Stack Contract (push) Has been cancelled
Sirius CI/CD Pipeline / Dispatch Demo Deployment (sirius-demo branch) (push) Has been cancelled
Sirius CI/CD Pipeline / Dispatch Demo Canary (main branch) (push) Has been cancelled
Sirius CI/CD Pipeline / Guard Registry Namespace (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:25 +08:00

356 lines
13 KiB
Go

package main
import (
"encoding/json"
"fmt"
"strings"
)
// RunAPISuite tests authentication and authorization on the Go REST API (port 9001).
func RunAPISuite(cfg *Config) SuiteResult {
suite := SuiteResult{Name: "Go API Authentication"}
base := cfg.APIURL
// ── Phase 0: Ensure API is reachable ────────────────────────────────
code, _, _, err := httpGet(base+"/health", nil)
if err != nil || code == 0 {
suite.Results = append(suite.Results, TestResult{
Name: "API reachability check",
Result: Skip,
Severity: SevCritical,
Detail: fmt.Sprintf("Cannot reach %s/health: %v", base, err),
})
return suite
}
// ── Phase 1: Obtain a valid API key for positive tests ──────────────
validKey := cfg.APIKey
if validKey == "" {
validKey = provisionAPIKeyForTests(cfg, &suite)
if validKey == "" {
// Cannot proceed without a valid key; all remaining tests skipped.
suite.Results = append(suite.Results, TestResult{
Name: "API key provisioning",
Result: Skip,
Severity: SevCritical,
Detail: "Could not obtain a valid API key; remaining tests skipped",
})
return suite
}
}
// ── Phase 2: Health endpoint bypass ─────────────────────────────────
suite.Results = append(suite.Results, testHealthBypass(base))
// ── Phase 3: No API key on protected endpoints ──────────────────────
protectedEndpoints := []struct {
method string
path string
}{
{"GET", "/host/"},
{"GET", "/vulnerability/nonexistent-id"},
{"GET", "/templates"},
{"GET", "/api/agent-templates"},
{"GET", "/api/v1/scans/status"},
{"GET", "/api/v1/events"},
{"GET", "/api/v1/statistics/vulnerability-trends"},
{"GET", "/api/v1/system/health"},
{"GET", "/api/v1/logs"},
{"GET", "/api/v1/keys"},
{"POST", "/api/v1/keys"},
}
for _, ep := range protectedEndpoints {
suite.Results = append(suite.Results, testNoAPIKey(base, ep.method, ep.path))
}
// ── Phase 4: Invalid API key variants ───────────────────────────────
invalidKeys := []struct {
label string
key string
}{
{"random string", "totally-invalid-key-12345"},
{"malformed sk_ prefix", "sk_not-hex-at-all!!!"},
{"empty value", ""},
{"truncated valid key", validKey[:10]},
}
for _, ik := range invalidKeys {
suite.Results = append(suite.Results, testInvalidKey(base, ik.label, ik.key))
}
// ── Phase 5: Valid API key succeeds ─────────────────────────────────
for _, ep := range protectedEndpoints {
suite.Results = append(suite.Results, testValidKey(base, validKey, ep.method, ep.path))
}
// ── Phase 6: Key in wrong header / query param ──────────────────────
suite.Results = append(suite.Results, testKeyWrongHeader(base, validKey, "Authorization"))
suite.Results = append(suite.Results, testKeyWrongHeader(base, validKey, "Api-Key"))
suite.Results = append(suite.Results, testKeyInQueryParam(base, validKey))
// ── Phase 7: Revoked key ────────────────────────────────────────────
suite.Results = append(suite.Results, testRevokedKey(cfg, validKey)...)
// ── Phase 8: Case sensitivity of header ─────────────────────────────
suite.Results = append(suite.Results, testHeaderCaseSensitivity(base, validKey))
return suite
}
// ────────────────── Individual test functions ──────────────────
func testHealthBypass(base string) TestResult {
code, _, _, err := httpGet(base+"/health", nil)
if err != nil {
return TestResult{Name: "Health endpoint bypass (no key)", Result: Fail, Severity: SevHigh,
Detail: fmt.Sprintf("request error: %v", err)}
}
if code == 200 {
return TestResult{Name: "Health endpoint bypass (no key)", Result: Pass,
Detail: "200 OK without API key"}
}
return TestResult{Name: "Health endpoint bypass (no key)", Result: Fail, Severity: SevHigh,
Detail: fmt.Sprintf("expected 200, got %d", code)}
}
func testNoAPIKey(base, method, path string) TestResult {
name := fmt.Sprintf("No API key on %s %s", method, path)
code, _, _, err := httpDo(method, base+path, nil, "")
if err != nil {
return TestResult{Name: name, Result: Fail, Severity: SevHigh,
Detail: fmt.Sprintf("request error: %v", err)}
}
if code == 401 {
return TestResult{Name: name, Result: Pass, Detail: "401 Unauthorized"}
}
return TestResult{Name: name, Result: Fail, Severity: SevHigh,
Detail: fmt.Sprintf("expected 401, got %d", code)}
}
func testInvalidKey(base, label, key string) TestResult {
name := fmt.Sprintf("Invalid API key (%s)", label)
headers := map[string]string{"X-API-Key": key}
code, _, _, err := httpGet(base+"/host/", headers)
if err != nil {
return TestResult{Name: name, Result: Fail, Severity: SevHigh,
Detail: fmt.Sprintf("request error: %v", err)}
}
if code == 401 {
return TestResult{Name: name, Result: Pass, Detail: "401 Unauthorized"}
}
return TestResult{Name: name, Result: Fail, Severity: SevHigh,
Detail: fmt.Sprintf("expected 401, got %d", code)}
}
func testValidKey(base, key, method, path string) TestResult {
name := fmt.Sprintf("Valid API key on %s %s", method, path)
headers := apiHeaders(key)
var code int
var err error
if method == "POST" {
code, _, _, err = httpPost(base+path, headers, `{"label":"test"}`)
} else {
code, _, _, err = httpDo(method, base+path, headers, "")
}
if err != nil {
return TestResult{Name: name, Result: Fail, Severity: SevHigh,
Detail: fmt.Sprintf("request error: %v", err)}
}
// Any non-401 response means auth succeeded. 404/500 etc. are functional
// issues, not auth failures.
if code != 401 {
return TestResult{Name: name, Result: Pass,
Detail: fmt.Sprintf("%d (auth passed)", code)}
}
return TestResult{Name: name, Result: Fail, Severity: SevHigh,
Detail: "got 401 with valid key"}
}
func testKeyWrongHeader(base, key, headerName string) TestResult {
name := fmt.Sprintf("API key in wrong header (%s)", headerName)
headers := map[string]string{headerName: key}
code, _, _, err := httpGet(base+"/host/", headers)
if err != nil {
return TestResult{Name: name, Result: Fail, Severity: SevMedium,
Detail: fmt.Sprintf("request error: %v", err)}
}
if code == 401 {
return TestResult{Name: name, Result: Pass,
Detail: "401 — key only accepted via X-API-Key header"}
}
return TestResult{Name: name, Result: Fail, Severity: SevMedium,
Detail: fmt.Sprintf("expected 401, got %d (key accepted in wrong header)", code)}
}
func testKeyInQueryParam(base, key string) TestResult {
name := "API key in query parameter"
url := fmt.Sprintf("%s/host/?api_key=%s", base, key)
code, _, _, err := httpGet(url, nil)
if err != nil {
return TestResult{Name: name, Result: Fail, Severity: SevMedium,
Detail: fmt.Sprintf("request error: %v", err)}
}
if code == 401 {
return TestResult{Name: name, Result: Pass,
Detail: "401 — key only accepted via header, not query param"}
}
return TestResult{Name: name, Result: Fail, Severity: SevMedium,
Detail: fmt.Sprintf("expected 401, got %d (key accepted in query param)", code)}
}
func testRevokedKey(cfg *Config, validKey string) []TestResult {
base := cfg.APIURL
var results []TestResult
// Create a new key, then revoke it, then try to use it.
headers := apiHeaders(validKey)
code, body, _, err := httpPost(base+"/api/v1/keys", headers, `{"label":"revoke-test"}`)
if err != nil || code < 200 || code >= 300 {
results = append(results, TestResult{
Name: "Revoked key test (setup: create key)", Result: Skip, Severity: SevHigh,
Detail: fmt.Sprintf("could not create test key: status=%d err=%v", code, err),
})
return results
}
// Parse the raw key and hash ID from the response.
var createResp struct {
RawKey string `json:"raw_key"`
Key string `json:"key"`
Meta struct {
ID string `json:"id"`
} `json:"meta"`
}
if err := json.Unmarshal([]byte(body), &createResp); err != nil {
results = append(results, TestResult{
Name: "Revoked key test (setup: parse key)", Result: Skip, Severity: SevHigh,
Detail: fmt.Sprintf("could not parse create response: %v", err),
})
return results
}
newKey := strings.TrimSpace(createResp.RawKey)
if newKey == "" {
newKey = strings.TrimSpace(createResp.Key)
}
keyID := strings.TrimSpace(createResp.Meta.ID)
if newKey == "" || keyID == "" {
results = append(results, TestResult{
Name: "Revoked key test (setup: parse key)", Result: Skip, Severity: SevHigh,
Detail: "create key response missing raw key or key id metadata",
})
return results
}
// Verify the new key works.
code, _, _, _ = httpGet(base+"/host/", apiHeaders(newKey))
if code == 401 {
results = append(results, TestResult{
Name: "Revoked key test (setup: verify new key works)", Result: Skip, Severity: SevHigh,
Detail: "newly created key already returns 401",
})
return results
}
// Revoke the key.
code, _, _, err = httpDelete(base+"/api/v1/keys/"+keyID, headers)
if err != nil || (code < 200 || code >= 300) {
results = append(results, TestResult{
Name: "Revoked key test (setup: revoke key)", Result: Skip, Severity: SevHigh,
Detail: fmt.Sprintf("could not revoke key: status=%d err=%v", code, err),
})
return results
}
// Try to use the revoked key.
code, _, _, err = httpGet(base+"/host/", apiHeaders(newKey))
if err != nil {
results = append(results, TestResult{
Name: "Revoked key rejected", Result: Fail, Severity: SevHigh,
Detail: fmt.Sprintf("request error: %v", err),
})
return results
}
if code == 401 {
results = append(results, TestResult{
Name: "Revoked key rejected", Result: Pass,
Detail: "401 — revoked key correctly rejected",
})
} else {
results = append(results, TestResult{
Name: "Revoked key rejected", Result: Fail, Severity: SevCritical,
Detail: fmt.Sprintf("expected 401, got %d — revoked key still accepted!", code),
})
}
return results
}
func testHeaderCaseSensitivity(base, key string) TestResult {
// HTTP headers are case-insensitive per spec, so x-api-key should work too.
name := "Header case insensitivity (x-api-key lowercase)"
headers := map[string]string{"x-api-key": key}
code, _, _, err := httpGet(base+"/host/", headers)
if err != nil {
return TestResult{Name: name, Result: Warn, Severity: SevLow,
Detail: fmt.Sprintf("request error: %v", err)}
}
if code != 401 {
return TestResult{Name: name, Result: Pass,
Detail: fmt.Sprintf("%d — header is case-insensitive (correct per HTTP spec)", code)}
}
return TestResult{Name: name, Result: Warn, Severity: SevLow,
Detail: "lowercase header rejected — may cause client compatibility issues"}
}
// ────────────────── Bootstrap helper ──────────────────
// provisionAPIKeyForTests creates a test API key using the /api/v1/keys endpoint.
// It first tries without auth (in case API_KEY_REQUIRED=false in dev mode).
// Falls back to checking if there's an existing root key reference.
func provisionAPIKeyForTests(cfg *Config, suite *SuiteResult) string {
base := cfg.APIURL
// Try creating a key without auth (works if API_KEY_REQUIRED=false).
code, body, _, err := httpPost(base+"/api/v1/keys", nil, `{"label":"security-test-bootstrap"}`)
if err == nil && code >= 200 && code < 300 {
var resp struct {
RawKey string `json:"raw_key"`
}
if json.Unmarshal([]byte(body), &resp) == nil && resp.RawKey != "" {
suite.Results = append(suite.Results, TestResult{
Name: "API key provisioning (no auth)", Result: Warn, Severity: SevHigh,
Detail: "Key creation succeeded without API key — API_KEY_REQUIRED may be false",
})
return resp.RawKey
}
}
// If we got 401, auth is enforced. We need a key from the environment.
if code == 401 {
suite.Results = append(suite.Results, TestResult{
Name: "API key provisioning", Result: Info, Severity: SevInfo,
Detail: "API requires authentication — set SIRIUS_API_KEY env var with a valid key",
})
}
// Last resort: check if the API returns something useful at health that hints at setup.
if strings.Contains(body, "error") {
suite.Results = append(suite.Results, TestResult{
Name: "API key provisioning", Result: Skip, Severity: SevInfo,
Detail: fmt.Sprintf("Cannot provision key: %s", truncate(body, 120)),
})
}
return ""
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}