mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-09-13 21:27:47 +02:00
feat: add canary tests for real-world scan workflows
- Add docker-compose.canary.yaml with multi-stage Dockerfile to build osmedeus from source and layer onto toolbox base, ensuring canary tests exercise current code not released binaries - Implement three canary test scenarios (repo SAST scan, domain-lite reconnaissance, CIDR IP scanning) with lifecycle management (container startup/cleanup) and comprehensive assertions on filesystem artifacts and API records - Add Makefile targets (test-canary-all, test-canary-repo, test-canary-domain, test-canary-ip, canary-up, canary-down) for granular test execution with configurable timeouts (20-60 minutes)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
.PHONY: build run test test-unit test-integration test-workflow-integration test-e2e test-e2e-verbose test-e2e-ssh test-e2e-api test-e2e-nix test-e2e-install test-docker test-ssh test-distributed test-all test-summary test-ci clean install install-gotestsum lint fmt db-seed db-clean db-migrate run-server-debug swagger update-ui snapshot-release github-release run-github-action docker-toolbox docker-toolbox-run docker-toolbox-shell docker-publish
|
||||
.PHONY: build run test test-unit test-integration test-workflow-integration test-e2e test-e2e-verbose test-e2e-ssh test-e2e-api test-e2e-nix test-e2e-install test-docker test-ssh test-distributed test-canary-all test-canary-repo test-canary-domain test-canary-ip canary-up canary-down test-all test-summary test-ci clean install install-gotestsum lint fmt db-seed db-clean db-migrate run-server-debug swagger update-ui snapshot-release github-release run-github-action docker-toolbox docker-toolbox-run docker-toolbox-shell docker-publish
|
||||
|
||||
# Go parameters
|
||||
GOCMD=go
|
||||
@@ -191,6 +191,47 @@ test-e2e-install: build install-gotestsum
|
||||
@echo "$(PREFIX) Running install E2E tests..."
|
||||
$(TESTCMD) $(TESTFLAGS) -run TestInstall ./test/e2e/...
|
||||
|
||||
# ── Canary tests (real scans inside Docker toolbox, requires Docker) ──────────
|
||||
|
||||
# Build and start the canary container (shared setup for individual targets)
|
||||
canary-up: install-gotestsum
|
||||
@echo "$(PREFIX) Building canary Docker image..."
|
||||
docker-compose -f build/docker/docker-compose.canary.yaml build
|
||||
@echo "$(PREFIX) Starting canary container..."
|
||||
docker-compose -f build/docker/docker-compose.canary.yaml up -d
|
||||
@echo "$(PREFIX) Waiting for API server..."
|
||||
@for i in $$(seq 1 60); do curl -sf http://localhost:8002/health > /dev/null 2>&1 && break || sleep 2; done
|
||||
@echo "$(PREFIX) Canary container ready."
|
||||
|
||||
# Tear down the canary container
|
||||
canary-down:
|
||||
@echo "$(PREFIX) Cleaning up canary container..."
|
||||
docker-compose -f build/docker/docker-compose.canary.yaml down -v
|
||||
|
||||
# Run ALL canary scans (builds container, runs all 3, cleans up — 30-60min)
|
||||
test-canary-all: canary-up
|
||||
@echo "$(PREFIX) Running all canary tests (30-60 minutes)..."
|
||||
$(TESTCMD) $(TESTFLAGS) -run TestCanary_FullSuite -timeout 60m ./test/e2e/... || ($(MAKE) canary-down && exit 1)
|
||||
@$(MAKE) canary-down
|
||||
|
||||
# Repo scan canary (juice-shop SAST, ~25min)
|
||||
test-canary-repo: canary-up
|
||||
@echo "$(PREFIX) Running repo scan canary test..."
|
||||
$(TESTCMD) $(TESTFLAGS) -run TestCanary_Repo -timeout 30m ./test/e2e/... || ($(MAKE) canary-down && exit 1)
|
||||
@$(MAKE) canary-down
|
||||
|
||||
# Domain-lite scan canary (hackerone.com, ~20min)
|
||||
test-canary-domain: canary-up
|
||||
@echo "$(PREFIX) Running domain-lite scan canary test..."
|
||||
$(TESTCMD) $(TESTFLAGS) -run TestCanary_Domain -timeout 25m ./test/e2e/... || ($(MAKE) canary-down && exit 1)
|
||||
@$(MAKE) canary-down
|
||||
|
||||
# CIDR scan canary (IP list, ~25min)
|
||||
test-canary-ip: canary-up
|
||||
@echo "$(PREFIX) Running CIDR scan canary test..."
|
||||
$(TESTCMD) $(TESTFLAGS) -run TestCanary_CIDR -timeout 30m ./test/e2e/... || ($(MAKE) canary-down && exit 1)
|
||||
@$(MAKE) canary-down
|
||||
|
||||
# All tests
|
||||
test-all: test-unit test-integration
|
||||
|
||||
@@ -369,6 +410,10 @@ help:
|
||||
@echo " make test-docker Run Docker runner tests"
|
||||
@echo " make test-ssh Run SSH runner unit tests"
|
||||
@echo " make test-distributed Run distributed scan E2E tests (requires Redis)"
|
||||
@echo " make test-canary-all Run all canary tests (real scans in Docker, 30-60min)"
|
||||
@echo " make test-canary-repo Run repo scan canary (juice-shop SAST, ~25min)"
|
||||
@echo " make test-canary-domain Run domain-lite canary (hackerone.com, ~20min)"
|
||||
@echo " make test-canary-ip Run CIDR scan canary (IP list, ~25min)"
|
||||
@echo " make test-coverage Run tests with coverage report"
|
||||
@echo " make test-summary Quick pass/fail summary (dots format)"
|
||||
@echo " make test-ci Run tests with JUnit XML output"
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Osmedeus Canary Test Dockerfile
|
||||
# Multi-stage build that compiles from source and layers onto the toolbox image,
|
||||
# ensuring canary tests exercise the CURRENT source code, not the last release.
|
||||
|
||||
# ── Stage 1: build from source ──────────────────────────────────────────────
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /osmedeus ./cmd/osmedeus
|
||||
|
||||
# ── Stage 2: runtime (mirrors Dockerfile.toolbox) ───────────────────────────
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
unzip \
|
||||
jq \
|
||||
bash \
|
||||
xz-utils \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
chromium-browser \
|
||||
sudo \
|
||||
golang-go \
|
||||
&& update-ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN ln -sf /usr/bin/python3 /usr/bin/python
|
||||
|
||||
ENV PATH="/root/go/bin:${PATH}"
|
||||
ENV GOPATH="/root/go"
|
||||
|
||||
RUN mkdir -p /root/osmedeus-base /root/workspaces-osmedeus /root/go/bin
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
# Install released osmedeus (pulls workflows, tools, etc.)
|
||||
RUN curl -fsSLk https://www.osmedeus.org/install.sh | bash
|
||||
|
||||
ENV PATH="/root/.local/bin:/root/go/bin:/root/osmedeus-base/external-binaries:${PATH}"
|
||||
|
||||
# Overwrite the released binary with the one built from source
|
||||
COPY --from=builder /osmedeus /root/.local/bin/osmedeus
|
||||
COPY --from=builder /osmedeus /root/go/bin/osmedeus
|
||||
|
||||
# Copy the canary entrypoint
|
||||
COPY build/docker/canary-entrypoint.sh /usr/local/bin/canary-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/canary-entrypoint.sh
|
||||
|
||||
# Verify the source-built binary works
|
||||
RUN osmedeus health
|
||||
|
||||
EXPOSE 8002
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/canary-entrypoint.sh"]
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Canary test entrypoint: starts API server in background and keeps container alive.
|
||||
# Tests interact via:
|
||||
# docker exec osm-canary osmedeus run ...
|
||||
# curl http://localhost:8002/osm/api/...
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "[canary] Starting osmedeus API server on :8002 ..."
|
||||
osmedeus serve --port 8002 --host 0.0.0.0 -A &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Give the server a moment to bind
|
||||
sleep 2
|
||||
|
||||
echo "[canary] API server started (PID ${SERVER_PID})"
|
||||
echo "[canary] Container is ready — waiting for test commands."
|
||||
|
||||
# Keep the container alive; forward SIGTERM to the server
|
||||
trap "kill ${SERVER_PID} 2>/dev/null; exit 0" SIGTERM SIGINT
|
||||
|
||||
wait ${SERVER_PID}
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
osm-canary:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: build/docker/Dockerfile.canary
|
||||
container_name: osm-canary
|
||||
hostname: osm-canary
|
||||
volumes:
|
||||
- canary-workspaces:/root/workspaces-osmedeus
|
||||
- canary-data:/root/osmedeus-base
|
||||
ports:
|
||||
- "8002:8002"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:8002/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
start_period: 30s
|
||||
retries: 5
|
||||
restart: "no"
|
||||
|
||||
volumes:
|
||||
canary-workspaces:
|
||||
canary-data:
|
||||
@@ -0,0 +1,492 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ── constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
canaryContainerName = "osm-canary"
|
||||
canaryAPIBase = "http://localhost:8002"
|
||||
canaryWorkspaceRoot = "/root/workspaces-osmedeus"
|
||||
)
|
||||
|
||||
// ── compose helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
func getCanaryComposePath(t *testing.T) string {
|
||||
t.Helper()
|
||||
return filepath.Join(getProjectRoot(t), "build", "docker", "docker-compose.canary.yaml")
|
||||
}
|
||||
|
||||
// startCanaryContainer builds the canary image and starts the container.
|
||||
// It returns a cleanup function that tears everything down.
|
||||
func startCanaryContainer(t *testing.T, log *TestLogger) func() {
|
||||
t.Helper()
|
||||
composePath := getCanaryComposePath(t)
|
||||
|
||||
log.Step("Building canary Docker image (this may take a while)")
|
||||
cmd := exec.Command("docker-compose", "-f", composePath, "build")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build canary image: %v\nOutput: %s", err, output)
|
||||
}
|
||||
log.Success("Canary image built")
|
||||
|
||||
log.Step("Starting canary container")
|
||||
cmd = exec.Command("docker-compose", "-f", composePath, "up", "-d")
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start canary container: %v\nOutput: %s", err, output)
|
||||
}
|
||||
log.Success("Canary container started")
|
||||
|
||||
return func() {
|
||||
log.Info("Tearing down canary container")
|
||||
cmd := exec.Command("docker-compose", "-f", composePath, "down", "-v")
|
||||
_ = cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
// waitForCanaryAPI polls the health endpoint until the API is ready.
|
||||
func waitForCanaryAPI(t *testing.T, log *TestLogger, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
url := canaryAPIBase + "/health"
|
||||
|
||||
log.Info("Waiting for canary API at %s ...", url)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := http.Get(url)
|
||||
if err == nil {
|
||||
_ = resp.Body.Close()
|
||||
if resp.StatusCode == 200 {
|
||||
log.Success("Canary API is ready")
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
t.Fatalf("Canary API not ready after %v", timeout)
|
||||
}
|
||||
|
||||
// ── docker exec helpers ──────────────────────────────────────────────────────
|
||||
|
||||
// dockerExec runs a command inside the canary container with a 2-minute timeout.
|
||||
func dockerExec(t *testing.T, log *TestLogger, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
return dockerExecLong(t, log, 2*time.Minute, args...)
|
||||
}
|
||||
|
||||
// dockerExecLong runs a command inside the canary container with a custom timeout.
|
||||
func dockerExecLong(t *testing.T, log *TestLogger, timeout time.Duration, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
cmdArgs := append([]string{"exec", canaryContainerName}, args...)
|
||||
log.Debug("docker %s", strings.Join(cmdArgs, " "))
|
||||
|
||||
cmd := exec.CommandContext(ctx, "docker", cmdArgs...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
out := string(output)
|
||||
|
||||
if len(out) > 500 {
|
||||
log.Debug("output (%d bytes): %s...", len(out), out[:500])
|
||||
} else if out != "" {
|
||||
log.Debug("output: %s", strings.TrimSpace(out))
|
||||
}
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
// ── filesystem checks inside container ───────────────────────────────────────
|
||||
|
||||
func fileExistsInContainer(t *testing.T, log *TestLogger, path string) bool {
|
||||
t.Helper()
|
||||
_, err := dockerExec(t, log, "test", "-f", path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func dirExistsInContainer(t *testing.T, log *TestLogger, path string) bool {
|
||||
t.Helper()
|
||||
_, err := dockerExec(t, log, "test", "-d", path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// findFilesInContainer returns files matching a pattern under dir.
|
||||
func findFilesInContainer(t *testing.T, log *TestLogger, dir, pattern string) []string {
|
||||
t.Helper()
|
||||
out, err := dockerExec(t, log, "find", dir, "-name", pattern, "-type", "f")
|
||||
if err != nil || strings.TrimSpace(out) == "" {
|
||||
return nil
|
||||
}
|
||||
var files []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
|
||||
if line != "" {
|
||||
files = append(files, line)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// ── API helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
// canaryAPIGet performs a GET request against the canary API and parses JSON.
|
||||
func canaryAPIGet(t *testing.T, path string) map[string]any {
|
||||
t.Helper()
|
||||
resp, err := http.Get(canaryAPIBase + path)
|
||||
require.NoError(t, err, "GET %s failed", path)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err, "failed to read response for %s", path)
|
||||
|
||||
var result map[string]any
|
||||
err = json.Unmarshal(body, &result)
|
||||
require.NoError(t, err, "failed to parse JSON from %s: %s", path, string(body))
|
||||
return result
|
||||
}
|
||||
|
||||
// getRunsForWorkspace returns runs filtered by workspace name.
|
||||
func getRunsForWorkspace(t *testing.T, ws string) []any {
|
||||
t.Helper()
|
||||
result := canaryAPIGet(t, "/osm/api/runs?workspace="+ws)
|
||||
data, _ := result["data"].([]any)
|
||||
return data
|
||||
}
|
||||
|
||||
// getAssetsForWorkspace returns assets filtered by workspace name.
|
||||
func getAssetsForWorkspace(t *testing.T, ws string) []any {
|
||||
t.Helper()
|
||||
result := canaryAPIGet(t, "/osm/api/assets?workspace="+ws)
|
||||
data, _ := result["data"].([]any)
|
||||
return data
|
||||
}
|
||||
|
||||
// getVulnsForWorkspace returns vulnerabilities filtered by workspace name.
|
||||
func getVulnsForWorkspace(t *testing.T, ws string) []any {
|
||||
t.Helper()
|
||||
result := canaryAPIGet(t, "/osm/api/vulnerabilities?workspace="+ws)
|
||||
data, _ := result["data"].([]any)
|
||||
return data
|
||||
}
|
||||
|
||||
// getWorkspaces returns all workspace records.
|
||||
func getWorkspaces(t *testing.T) []map[string]any {
|
||||
t.Helper()
|
||||
result := canaryAPIGet(t, "/osm/api/workspaces")
|
||||
raw, _ := result["data"].([]any)
|
||||
var out []map[string]any
|
||||
for _, item := range raw {
|
||||
if m, ok := item.(map[string]any); ok {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// findWorkspaceByName searches all workspaces for one matching name (exact or substring).
|
||||
func findWorkspaceByName(t *testing.T, name string) map[string]any {
|
||||
t.Helper()
|
||||
for _, ws := range getWorkspaces(t) {
|
||||
wsName, _ := ws["name"].(string)
|
||||
if wsName == name || strings.Contains(wsName, name) {
|
||||
return ws
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── main test entry points ───────────────────────────────────────────────────
|
||||
|
||||
// requireCanaryAPI checks that the canary container API is reachable.
|
||||
// Individual tests (TestCanary_Repo, etc.) assume the container was started
|
||||
// externally by the Makefile, so they only need to verify the API is up.
|
||||
func requireCanaryAPI(t *testing.T, log *TestLogger) {
|
||||
t.Helper()
|
||||
waitForCanaryAPI(t, log, 60*time.Second)
|
||||
}
|
||||
|
||||
// TestCanary_FullSuite manages the full lifecycle: build, start, run all 3 scans, cleanup.
|
||||
// Use `make test-canary-all` to run this.
|
||||
func TestCanary_FullSuite(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping canary tests in short mode")
|
||||
}
|
||||
|
||||
log := NewTestLogger(t)
|
||||
log.Step("Starting canary test suite")
|
||||
|
||||
// Start the canary container (build + up)
|
||||
cleanup := startCanaryContainer(t, log)
|
||||
defer cleanup()
|
||||
|
||||
// Wait for the API server inside the container to be ready
|
||||
waitForCanaryAPI(t, log, 90*time.Second)
|
||||
|
||||
// Run the three canary scans sequentially
|
||||
t.Run("RepoScan", func(t *testing.T) {
|
||||
testCanaryRepoScan(t)
|
||||
})
|
||||
t.Run("DomainLiteScan", func(t *testing.T) {
|
||||
testCanaryDomainLiteScan(t)
|
||||
})
|
||||
t.Run("CIDRScan", func(t *testing.T) {
|
||||
testCanaryCIDRScan(t)
|
||||
})
|
||||
|
||||
log.Success("All canary tests completed")
|
||||
}
|
||||
|
||||
// TestCanary_Repo runs only the repo scan canary test.
|
||||
// Assumes the canary container is already running (e.g. via `make test-canary-repo`).
|
||||
func TestCanary_Repo(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping canary tests in short mode")
|
||||
}
|
||||
log := NewTestLogger(t)
|
||||
requireCanaryAPI(t, log)
|
||||
testCanaryRepoScan(t)
|
||||
}
|
||||
|
||||
// TestCanary_Domain runs only the domain-lite scan canary test.
|
||||
// Assumes the canary container is already running (e.g. via `make test-canary-domain`).
|
||||
func TestCanary_Domain(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping canary tests in short mode")
|
||||
}
|
||||
log := NewTestLogger(t)
|
||||
requireCanaryAPI(t, log)
|
||||
testCanaryDomainLiteScan(t)
|
||||
}
|
||||
|
||||
// TestCanary_CIDR runs only the CIDR scan canary test.
|
||||
// Assumes the canary container is already running (e.g. via `make test-canary-ip`).
|
||||
func TestCanary_CIDR(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping canary tests in short mode")
|
||||
}
|
||||
log := NewTestLogger(t)
|
||||
requireCanaryAPI(t, log)
|
||||
testCanaryCIDRScan(t)
|
||||
}
|
||||
|
||||
// ── Test 1: Repo Scan ────────────────────────────────────────────────────────
|
||||
|
||||
func testCanaryRepoScan(t *testing.T) {
|
||||
log := NewTestLogger(t)
|
||||
log.Step("Canary: Repo Scan (juice-shop)")
|
||||
|
||||
targetURL := "https://github.com/juice-shop/juice-shop/archive/refs/heads/master.zip"
|
||||
expectedWS := "github.com"
|
||||
wsDir := canaryWorkspaceRoot + "/" + expectedWS
|
||||
|
||||
// Run the scan
|
||||
log.Step("Running repo flow")
|
||||
out, err := dockerExecLong(t, log, 25*time.Minute,
|
||||
"osmedeus", "run", "-f", "repo", "-t", targetURL)
|
||||
if err != nil {
|
||||
log.Error("Repo scan command error: %v\nOutput: %s", err, out)
|
||||
}
|
||||
// Don't require.NoError — some steps may fail yet still produce results
|
||||
|
||||
// ── Filesystem checks ────────────────────────────────────────────────
|
||||
log.Step("Verifying filesystem artifacts")
|
||||
|
||||
assert.True(t, dirExistsInContainer(t, log, wsDir),
|
||||
"workspace directory %s should exist", wsDir)
|
||||
|
||||
sarifFiles := findFilesInContainer(t, log, wsDir, "*.sarif")
|
||||
log.Info("Found %d SARIF files", len(sarifFiles))
|
||||
assert.NotEmpty(t, sarifFiles, "expected SARIF output files in workspace")
|
||||
|
||||
mdFiles := findFilesInContainer(t, log, wsDir, "*.md")
|
||||
log.Info("Found %d markdown reports", len(mdFiles))
|
||||
|
||||
// ── Database / API checks ────────────────────────────────────────────
|
||||
log.Step("Verifying database records via API")
|
||||
|
||||
// Runs
|
||||
runs := getRunsForWorkspace(t, expectedWS)
|
||||
if len(runs) == 0 {
|
||||
// Fallback: search all workspaces
|
||||
ws := findWorkspaceByName(t, expectedWS)
|
||||
if ws != nil {
|
||||
wsName, _ := ws["name"].(string)
|
||||
runs = getRunsForWorkspace(t, wsName)
|
||||
log.Info("Fallback workspace name: %s", wsName)
|
||||
}
|
||||
}
|
||||
assert.NotEmpty(t, runs, "expected at least 1 run for workspace %s", expectedWS)
|
||||
|
||||
if len(runs) > 0 {
|
||||
firstRun, _ := runs[0].(map[string]any)
|
||||
runUUID, _ := firstRun["run_uuid"].(string)
|
||||
if runUUID != "" {
|
||||
// Steps
|
||||
stepsResp := canaryAPIGet(t, fmt.Sprintf("/osm/api/runs/%s/steps", runUUID))
|
||||
stepsData, _ := stepsResp["data"].([]any)
|
||||
assert.NotEmpty(t, stepsData, "expected step results for run %s", runUUID)
|
||||
log.Info("Run %s has %d step results", runUUID, len(stepsData))
|
||||
|
||||
// Artifacts
|
||||
artifactsResp := canaryAPIGet(t, fmt.Sprintf("/osm/api/runs/%s/artifacts", runUUID))
|
||||
artifactsData, _ := artifactsResp["data"].([]any)
|
||||
log.Info("Run %s has %d artifacts", runUUID, len(artifactsData))
|
||||
}
|
||||
}
|
||||
|
||||
// Vulnerabilities
|
||||
vulns := getVulnsForWorkspace(t, expectedWS)
|
||||
log.Info("Found %d vulnerabilities for workspace %s", len(vulns), expectedWS)
|
||||
assert.NotEmpty(t, vulns, "juice-shop should produce vulnerability findings")
|
||||
|
||||
// Workspace record
|
||||
ws := findWorkspaceByName(t, expectedWS)
|
||||
assert.NotNil(t, ws, "workspace record should exist in DB")
|
||||
if ws != nil {
|
||||
totalVulns, _ := ws["total_vulns"].(float64)
|
||||
log.Info("Workspace total_vulns: %.0f", totalVulns)
|
||||
assert.Greater(t, totalVulns, float64(0), "workspace should have total_vulns > 0")
|
||||
}
|
||||
|
||||
log.Success("Repo scan canary passed")
|
||||
}
|
||||
|
||||
// ── Test 2: Domain-Lite Scan ─────────────────────────────────────────────────
|
||||
|
||||
func testCanaryDomainLiteScan(t *testing.T) {
|
||||
log := NewTestLogger(t)
|
||||
log.Step("Canary: Domain-Lite Scan (hackerone.com)")
|
||||
|
||||
target := "hackerone.com"
|
||||
expectedWS := "hackerone.com"
|
||||
wsDir := canaryWorkspaceRoot + "/" + expectedWS
|
||||
|
||||
// Run the scan
|
||||
log.Step("Running domain-lite flow")
|
||||
out, err := dockerExecLong(t, log, 20*time.Minute,
|
||||
"osmedeus", "run", "-f", "domain-lite", "-t", target)
|
||||
if err != nil {
|
||||
log.Error("Domain-lite scan command error: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// ── Filesystem checks ────────────────────────────────────────────────
|
||||
log.Step("Verifying filesystem artifacts")
|
||||
|
||||
assert.True(t, dirExistsInContainer(t, log, wsDir),
|
||||
"workspace directory %s should exist", wsDir)
|
||||
|
||||
txtFiles := findFilesInContainer(t, log, wsDir, "*.txt")
|
||||
log.Info("Found %d .txt files (subdomains, etc.)", len(txtFiles))
|
||||
assert.NotEmpty(t, txtFiles, "expected subdomain/output text files")
|
||||
|
||||
jsonlFiles := findFilesInContainer(t, log, wsDir, "*.jsonl")
|
||||
log.Info("Found %d .jsonl files (fingerprints, etc.)", len(jsonlFiles))
|
||||
|
||||
// ── Database / API checks ────────────────────────────────────────────
|
||||
log.Step("Verifying database records via API")
|
||||
|
||||
// Runs
|
||||
runs := getRunsForWorkspace(t, expectedWS)
|
||||
if len(runs) == 0 {
|
||||
ws := findWorkspaceByName(t, expectedWS)
|
||||
if ws != nil {
|
||||
wsName, _ := ws["name"].(string)
|
||||
runs = getRunsForWorkspace(t, wsName)
|
||||
log.Info("Fallback workspace name: %s", wsName)
|
||||
}
|
||||
}
|
||||
assert.NotEmpty(t, runs, "expected at least 1 run for workspace %s", expectedWS)
|
||||
|
||||
// Assets
|
||||
assets := getAssetsForWorkspace(t, expectedWS)
|
||||
log.Info("Found %d assets for workspace %s", len(assets), expectedWS)
|
||||
assert.NotEmpty(t, assets, "expected discovered subdomains/HTTP endpoints")
|
||||
|
||||
// Workspace record
|
||||
ws := findWorkspaceByName(t, expectedWS)
|
||||
assert.NotNil(t, ws, "workspace record should exist in DB")
|
||||
|
||||
log.Success("Domain-lite scan canary passed")
|
||||
}
|
||||
|
||||
// ── Test 3: CIDR Scan ────────────────────────────────────────────────────────
|
||||
|
||||
func testCanaryCIDRScan(t *testing.T) {
|
||||
log := NewTestLogger(t)
|
||||
log.Step("Canary: CIDR Scan (IP list)")
|
||||
|
||||
expectedWS := "list-of-ips-file"
|
||||
wsDir := canaryWorkspaceRoot + "/" + expectedWS
|
||||
|
||||
// Create the IP list inside the container.
|
||||
// NOTE: These are sample/public IP addresses used solely for testing the
|
||||
// scan workflow pipeline (workspace creation, step execution, DB writes).
|
||||
// There is no intent to attack or exploit any of these hosts.
|
||||
log.Step("Creating target IP list")
|
||||
ipList := strings.Join([]string{
|
||||
"102.88.154.187",
|
||||
"8.222.222.78",
|
||||
"93.184.216.34",
|
||||
"104.16.132.229",
|
||||
}, "\\n")
|
||||
_, err := dockerExec(t, log, "sh", "-c",
|
||||
fmt.Sprintf("printf '%s\\n' > /tmp/list-of-ips.txt", ipList))
|
||||
require.NoError(t, err, "failed to create IP list in container")
|
||||
|
||||
// Verify file was created
|
||||
assert.True(t, fileExistsInContainer(t, log, "/tmp/list-of-ips.txt"),
|
||||
"IP list file should exist in container")
|
||||
|
||||
// Run the scan
|
||||
log.Step("Running cidr flow")
|
||||
out, err := dockerExecLong(t, log, 25*time.Minute,
|
||||
"osmedeus", "run", "-f", "cidr", "-t", "/tmp/list-of-ips.txt")
|
||||
if err != nil {
|
||||
log.Error("CIDR scan command error: %v\nOutput: %s", err, out)
|
||||
}
|
||||
|
||||
// ── Filesystem checks ────────────────────────────────────────────────
|
||||
log.Step("Verifying filesystem artifacts")
|
||||
|
||||
assert.True(t, dirExistsInContainer(t, log, wsDir),
|
||||
"workspace directory %s should exist", wsDir)
|
||||
|
||||
outputFiles := findFilesInContainer(t, log, wsDir, "*")
|
||||
log.Info("Found %d output files in workspace", len(outputFiles))
|
||||
|
||||
// ── Database / API checks ────────────────────────────────────────────
|
||||
log.Step("Verifying database records via API")
|
||||
|
||||
// Runs
|
||||
runs := getRunsForWorkspace(t, expectedWS)
|
||||
if len(runs) == 0 {
|
||||
ws := findWorkspaceByName(t, expectedWS)
|
||||
if ws != nil {
|
||||
wsName, _ := ws["name"].(string)
|
||||
runs = getRunsForWorkspace(t, wsName)
|
||||
log.Info("Fallback workspace name: %s", wsName)
|
||||
}
|
||||
}
|
||||
assert.NotEmpty(t, runs, "expected at least 1 run for workspace %s", expectedWS)
|
||||
|
||||
// Workspace record
|
||||
ws := findWorkspaceByName(t, expectedWS)
|
||||
assert.NotNil(t, ws, "workspace record should exist in DB")
|
||||
|
||||
log.Success("CIDR scan canary passed")
|
||||
}
|
||||
Reference in New Issue
Block a user