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
244 lines
8.2 KiB
Go
244 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// RunTRPCSuite tests authentication on tRPC endpoints (Next.js, port 3000).
|
|
// tRPC endpoints are called as plain HTTP — queries use GET, mutations use POST.
|
|
// Format: /api/trpc/{router.procedure}?input=<url-encoded-json> for queries
|
|
// /api/trpc/{router.procedure} with JSON body for mutations
|
|
func RunTRPCSuite(cfg *Config) SuiteResult {
|
|
suite := SuiteResult{Name: "tRPC Authentication"}
|
|
base := cfg.UIURL
|
|
|
|
// ── Phase 0: Reachability ───────────────────────────────────────────
|
|
code, _, _, err := httpGet(base, nil)
|
|
if err != nil || code == 0 {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: "tRPC reachability check",
|
|
Result: Skip,
|
|
Severity: SevCritical,
|
|
Detail: fmt.Sprintf("Cannot reach %s: %v", base, err),
|
|
})
|
|
return suite
|
|
}
|
|
|
|
// ── Phase 1: Protected procedures WITHOUT session cookie ────────────
|
|
// These should all return UNAUTHORIZED when called without a valid session.
|
|
protectedProcedures := []struct {
|
|
router string
|
|
procedure string
|
|
isQuery bool // true = GET query, false = POST mutation
|
|
input string
|
|
}{
|
|
{"apikeys", "listKeys", true, ""},
|
|
{"apikeys", "createKey", false, `{"json":{"label":"test"}}`},
|
|
{"terminal", "executeCommand", false, `{"json":{"command":"whoami"}}`},
|
|
{"terminal", "getHistory", true, ""},
|
|
{"agent", "listAgentsWithHosts", true, ""},
|
|
{"user", "getProfile", true, ""},
|
|
{"agentScan", "getAgentScanStatus", true, `{"json":{"scanId":"test"}}`},
|
|
}
|
|
|
|
for _, p := range protectedProcedures {
|
|
name := fmt.Sprintf("Protected %s.%s without session", p.router, p.procedure)
|
|
endpoint := fmt.Sprintf("%s/api/trpc/%s.%s", base, p.router, p.procedure)
|
|
|
|
var code int
|
|
var body string
|
|
var reqErr error
|
|
|
|
if p.isQuery {
|
|
url := endpoint
|
|
if p.input != "" {
|
|
url += "?input=" + p.input
|
|
}
|
|
code, body, _, reqErr = httpGet(url, map[string]string{
|
|
"Content-Type": "application/json",
|
|
})
|
|
} else {
|
|
payload := p.input
|
|
if payload == "" {
|
|
payload = "{}"
|
|
}
|
|
code, body, _, reqErr = httpPost(endpoint, nil, payload)
|
|
}
|
|
|
|
if reqErr != nil {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Fail, Severity: SevHigh,
|
|
Detail: fmt.Sprintf("request error: %v", reqErr),
|
|
})
|
|
continue
|
|
}
|
|
|
|
if isUnauthorized(code, body) {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Pass,
|
|
Detail: fmt.Sprintf("%d — correctly denied", code),
|
|
})
|
|
} else {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Fail, Severity: SevHigh,
|
|
Detail: fmt.Sprintf("expected UNAUTHORIZED, got %d", code),
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Phase 2: Protected procedures with INVALID session cookie ───────
|
|
fakeCookie := "next-auth.session-token=fake-invalid-session-value-12345"
|
|
for _, p := range protectedProcedures[:3] { // test a representative sample
|
|
name := fmt.Sprintf("Protected %s.%s with invalid session", p.router, p.procedure)
|
|
endpoint := fmt.Sprintf("%s/api/trpc/%s.%s", base, p.router, p.procedure)
|
|
headers := map[string]string{
|
|
"Cookie": fakeCookie,
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
var code int
|
|
var body string
|
|
var reqErr error
|
|
|
|
if p.isQuery {
|
|
code, body, _, reqErr = httpGet(endpoint, headers)
|
|
} else {
|
|
code, body, _, reqErr = httpPost(endpoint, headers, p.input)
|
|
}
|
|
|
|
if reqErr != nil {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Fail, Severity: SevHigh,
|
|
Detail: fmt.Sprintf("request error: %v", reqErr),
|
|
})
|
|
continue
|
|
}
|
|
|
|
if isUnauthorized(code, body) {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Pass,
|
|
Detail: fmt.Sprintf("%d — invalid session correctly rejected", code),
|
|
})
|
|
} else {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Fail, Severity: SevCritical,
|
|
Detail: fmt.Sprintf("expected UNAUTHORIZED, got %d — invalid session accepted!", code),
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Phase 3: Public procedures accessibility (informational) ────────
|
|
// These are expected to work without authentication — report for awareness.
|
|
publicProcedures := []struct {
|
|
router string
|
|
procedure string
|
|
input string
|
|
}{
|
|
{"host", "getHostList", ""},
|
|
{"vulnerability", "getAllVulnerabilities", ""},
|
|
{"scanner", "getLatestScan", ""},
|
|
{"templates", "getTemplates", ""},
|
|
{"statistics", "getVulnerabilityTrends", ""},
|
|
{"events", "getEvents", ""},
|
|
}
|
|
|
|
for _, p := range publicProcedures {
|
|
name := fmt.Sprintf("Public %s.%s accessible without session", p.router, p.procedure)
|
|
endpoint := fmt.Sprintf("%s/api/trpc/%s.%s", base, p.router, p.procedure)
|
|
url := endpoint
|
|
if p.input != "" {
|
|
url += "?input=" + p.input
|
|
}
|
|
|
|
code, _, _, reqErr := httpGet(url, map[string]string{
|
|
"Content-Type": "application/json",
|
|
})
|
|
|
|
if reqErr != nil {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Warn, Severity: SevLow,
|
|
Detail: fmt.Sprintf("request error: %v", reqErr),
|
|
})
|
|
continue
|
|
}
|
|
|
|
if code >= 200 && code < 500 && !isUnauthorized(code, "") {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Info, Severity: SevInfo,
|
|
Detail: fmt.Sprintf("%d — public endpoint (no auth required)", code),
|
|
})
|
|
} else {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Pass,
|
|
Detail: fmt.Sprintf("%d — unexpectedly protected (good!)", code),
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Phase 4: Public WRITE procedures audit ──────────────────────────
|
|
// Flag public mutations that perform write/destructive operations.
|
|
publicWriteProcedures := []struct {
|
|
router string
|
|
procedure string
|
|
risk string
|
|
}{
|
|
{"scanner", "startScan", "Can initiate network scans without auth"},
|
|
{"scanner", "cancelScan", "Can cancel running scans without auth"},
|
|
{"scanner", "forceStopScan", "Can force-stop scans without auth"},
|
|
{"scanner", "resetScanState", "Can reset scan state without auth"},
|
|
{"templates", "createTemplate", "Can create templates without auth"},
|
|
{"templates", "deleteTemplate", "Can delete templates without auth"},
|
|
{"scripts", "createScript", "Can create scripts without auth"},
|
|
{"scripts", "deleteScript", "Can delete scripts without auth"},
|
|
{"store", "setValue", "Can write to Valkey store without auth"},
|
|
{"queue", "sendMsg", "Can send messages to RabbitMQ without auth"},
|
|
{"host", "createHost", "Can create host records without auth"},
|
|
{"repositories", "add", "Can add template repositories without auth"},
|
|
{"repositories", "delete", "Can delete repositories without auth"},
|
|
{"agentTemplates", "uploadTemplate", "Can upload agent templates without auth"},
|
|
{"agentTemplates", "deleteTemplate", "Can delete agent templates without auth"},
|
|
{"agentTemplates", "deployTemplate", "Can deploy templates to agents without auth"},
|
|
}
|
|
|
|
for _, p := range publicWriteProcedures {
|
|
name := fmt.Sprintf("Public write: %s.%s", p.router, p.procedure)
|
|
endpoint := fmt.Sprintf("%s/api/trpc/%s.%s", base, p.router, p.procedure)
|
|
|
|
// Send an empty/minimal mutation body — we just want to check if auth is enforced.
|
|
code, body, _, reqErr := httpPost(endpoint, nil, `{}`)
|
|
|
|
if reqErr != nil {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Warn, Severity: SevMedium,
|
|
Detail: fmt.Sprintf("request error: %v", reqErr),
|
|
})
|
|
continue
|
|
}
|
|
|
|
if isUnauthorized(code, body) {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Pass,
|
|
Detail: "requires authentication",
|
|
})
|
|
} else {
|
|
suite.Results = append(suite.Results, TestResult{
|
|
Name: name, Result: Warn, Severity: SevMedium,
|
|
Detail: fmt.Sprintf("%d — %s", code, p.risk),
|
|
})
|
|
}
|
|
}
|
|
|
|
return suite
|
|
}
|
|
|
|
// isUnauthorized checks if a response indicates an authentication failure.
|
|
// tRPC returns 401 for protectedProcedure, or a JSON error with "UNAUTHORIZED".
|
|
func isUnauthorized(code int, body string) bool {
|
|
if code == 401 || code == 403 {
|
|
return true
|
|
}
|
|
lower := strings.ToLower(body)
|
|
return strings.Contains(lower, "unauthorized") || strings.Contains(lower, "unauthenticated")
|
|
}
|