diff --git a/actions/cairn-zig-fuzz/action.yml b/actions/cairn-zig-fuzz/action.yml index 3fe24bb..cf90c0f 100644 --- a/actions/cairn-zig-fuzz/action.yml +++ b/actions/cairn-zig-fuzz/action.yml @@ -60,6 +60,15 @@ runs: set -eu command -v zig >/dev/null 2>&1 || { echo "ERROR: zig not found in PATH. Install Zig before using this action (e.g. https://codeberg.org/mlugg/setup-zig@v2)."; exit 1; } command -v afl-cc >/dev/null 2>&1 || { echo "ERROR: afl-cc not found in PATH after setup-afl step."; exit 1; } + if ! command -v jq >/dev/null 2>&1; then + if command -v apt-get >/dev/null 2>&1; then + apt-get update -qq + apt-get install -y -qq jq >/dev/null 2>&1 + else + echo "ERROR: jq not found and apt-get unavailable" + exit 1 + fi + fi echo "zig $(zig version), afl-cc found" - name: Setup Cairn CLI @@ -86,11 +95,16 @@ runs: # ── Start Cairn campaign ── SHORT_SHA=$(printf '%.8s' "${COMMIT}") + if [ -n "${TARGET}" ]; then + CAMPAIGN_NAME="fuzz-${TARGET}" + else + CAMPAIGN_NAME="fuzz-${SHORT_SHA}" + fi CAMPAIGN_OUTPUT=$(cairn campaign start \ -server "${CAIRN_SERVER}" \ -repo "${REPO}" \ -owner "${OWNER}" \ - -name "fuzz-${SHORT_SHA}" \ + -name "${CAMPAIGN_NAME}" \ -type fuzzing) CAMPAIGN_ID="${CAMPAIGN_OUTPUT#Campaign started: }" echo "Campaign ${CAMPAIGN_ID} started" @@ -115,6 +129,18 @@ runs: echo "Target ${TARGET_NUM}: zig build ${BUILD_ARGS}" echo "==========================================" + # Special-case Zig fuzz targets: if build args contain -Dfuzz-target=, + # use that as the effective Cairn target for metadata/track keying. + LINE_FUZZ_TARGET=$(printf '%s' "${BUILD_ARGS}" | sed -n 's/.*-Dfuzz-target=\([^[:space:]]*\).*/\1/p') + EFFECTIVE_TARGET="${TARGET}" + if [ -n "${LINE_FUZZ_TARGET}" ]; then + EFFECTIVE_TARGET="${LINE_FUZZ_TARGET}" + fi + + TRACK_SOURCE="${OWNER}/${REPO}|${EFFECTIVE_TARGET}|${BUILD_ARGS}" + TRACK_KEY=$(printf '%s' "${TRACK_SOURCE}" | sha256sum | awk '{print $1}') + echo "Track key: ${TRACK_KEY}" + # ── Build ── rm -rf zig-out zig build ${BUILD_ARGS} @@ -142,11 +168,27 @@ runs: echo "Fuzz binary: ${FUZZ_BIN}" # ── Seed corpus ── + SEEDS="afl-seeds-${TARGET_NUM}" + rm -rf "${SEEDS}" + mkdir -p "${SEEDS}" + if [ -n "${CORPUS_DIR}" ] && [ -d "${CORPUS_DIR}" ]; then - SEEDS="${CORPUS_DIR}" - else - SEEDS="afl-seeds" - mkdir -p "${SEEDS}" + cp -a "${CORPUS_DIR}/." "${SEEDS}/" 2>/dev/null || true + fi + + LATEST_CORPUS_ID=$(curl -fsSL "${CAIRN_SERVER}/api/v1/artifacts?type=fuzz&limit=200" \ + | jq -r --arg track "${TRACK_KEY}" --arg repo "${REPO}" \ + '.artifacts[]? | select(.repo_name == $repo and (.metadata.kind // "") == "corpus" and (.metadata.track_key // "") == $track) | .id' \ + | head -n1) + if [ -n "${LATEST_CORPUS_ID}" ] && [ "${LATEST_CORPUS_ID}" != "null" ]; then + echo "Downloading prior corpus artifact: ${LATEST_CORPUS_ID}" + if cairn download -server "${CAIRN_SERVER}" -id "${LATEST_CORPUS_ID}" -o "prior-corpus-${TARGET_NUM}.tar.gz"; then + tar xzf "prior-corpus-${TARGET_NUM}.tar.gz" -C "${SEEDS}" || true + rm -f "prior-corpus-${TARGET_NUM}.tar.gz" + fi + fi + + if [ "$(find "${SEEDS}" -maxdepth 1 -type f | wc -l)" -eq 0 ]; then printf 'A' > "${SEEDS}/seed-0" fi @@ -192,11 +234,12 @@ runs: echo "Uploading crash: ${CRASH_NAME}" set -- -server "${CAIRN_SERVER}" -repo "${REPO}" -owner "${OWNER}" \ - -commit "${COMMIT}" -type fuzz -file "${crash_file}" \ + -commit "${COMMIT}" -campaign-id "${CAMPAIGN_ID}" -type fuzz -file "${crash_file}" \ + -kind crash -track-key "${TRACK_KEY}" \ -crash-message "AFL++ crash (${BUILD_ARGS}): ${CRASH_NAME}" - if [ -n "${TARGET}" ]; then - set -- "$@" -target "${TARGET}" + if [ -n "${EFFECTIVE_TARGET}" ]; then + set -- "$@" -target "${EFFECTIVE_TARGET}" fi if [ -n "${SIG}" ]; then set -- "$@" -signal "${SIG}" @@ -216,9 +259,11 @@ runs: tar czf "corpus-${TARGET_NUM}.tar.gz" -C "${QUEUE_DIR}" . set -- -server "${CAIRN_SERVER}" -repo "${REPO}" -owner "${OWNER}" \ - -commit "${COMMIT}" -type fuzz -file "corpus-${TARGET_NUM}.tar.gz" - if [ -n "${TARGET}" ]; then - set -- "$@" -target "${TARGET}" + -commit "${COMMIT}" -campaign-id "${CAMPAIGN_ID}" -type fuzz \ + -kind corpus -track-key "${TRACK_KEY}" \ + -file "corpus-${TARGET_NUM}.tar.gz" + if [ -n "${EFFECTIVE_TARGET}" ]; then + set -- "$@" -target "${EFFECTIVE_TARGET}" fi cairn upload "$@" diff --git a/cmd/cairn/integration_test.go b/cmd/cairn/integration_test.go index 750bea9..cd8607b 100644 --- a/cmd/cairn/integration_test.go +++ b/cmd/cairn/integration_test.go @@ -133,6 +133,20 @@ func TestCLIUploadAndDownloadRoundTrip(t *testing.T) { serverURL, db, cleanup, _ := setupCLIServer(t, false) defer cleanup() + if err := cmdCampaign("start", []string{ + "-server", serverURL, + "-repo", "demo", + "-owner", "acme", + "-name", "seed-campaign", + "-type", "fuzz", + }); err != nil { + t.Fatalf("cmdCampaign start failed: %v", err) + } + var campaign models.Campaign + if err := db.First(&campaign).Error; err != nil { + t.Fatalf("querying campaign: %v", err) + } + artifactFile := filepath.Join(t.TempDir(), "artifact.bin") original := []byte("artifact bytes") if err := os.WriteFile(artifactFile, original, 0o644); err != nil { @@ -144,10 +158,13 @@ func TestCLIUploadAndDownloadRoundTrip(t *testing.T) { "-repo", "demo", "-owner", "acme", "-commit", "abcdef1234567890", + "-campaign-id", strconv.FormatUint(uint64(campaign.ID), 10), "-type", "fuzz", "-file", artifactFile, "-crash-message", "boom", "-signal", "11", + "-kind", "crash", + "-track-key", "track-123", }) if err != nil { t.Fatalf("cmdUpload failed: %v", err) @@ -165,6 +182,15 @@ func TestCLIUploadAndDownloadRoundTrip(t *testing.T) { if got := md["signal"]; got != "11" { t.Fatalf("expected metadata.signal=11, got %#v", got) } + if got := md["kind"]; got != "crash" { + t.Fatalf("expected metadata.kind=crash, got %#v", got) + } + if got := md["track_key"]; got != "track-123" { + t.Fatalf("expected metadata.track_key=track-123, got %#v", got) + } + if a.CampaignID == nil || *a.CampaignID != campaign.ID { + t.Fatalf("expected campaign_id=%d, got %#v", campaign.ID, a.CampaignID) + } outFile := filepath.Join(t.TempDir(), "downloaded.bin") if err := cmdDownload([]string{"-server", serverURL, "-id", strconv.FormatUint(uint64(a.ID), 10), "-o", outFile}); err != nil { diff --git a/cmd/cairn/main.go b/cmd/cairn/main.go index b750514..ab508d4 100644 --- a/cmd/cairn/main.go +++ b/cmd/cairn/main.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "path/filepath" + "strconv" ) var version = "dev" @@ -69,10 +70,13 @@ Upload flags: -repo NAME Repository name (required) -owner OWNER Repository owner (required) -commit SHA Commit SHA (required) + -campaign-id ID Campaign ID (optional) -type TYPE Artifact type: coredump, fuzz, sanitizer, simulation (required) -file PATH Path to artifact file (required) -crash-message MSG Crash message (optional) -stack-trace TRACE Stack trace text (optional) + -kind VALUE Artifact kind label (optional, stored in metadata) + -track-key VALUE Fuzz track key (optional, stored in metadata) -signal VALUE Crash signal number/name (optional, stored in metadata) -seed VALUE Simulation seed for reproducibility (optional, stored in metadata) -target NAME Target name/platform (optional, stored in metadata) @@ -85,10 +89,13 @@ func cmdUpload(args []string) error { repo string owner string commitSHA string + campaignID string artifactType string filePath string crashMessage string stackTrace string + kind string + trackKey string signal string seed string target string @@ -108,6 +115,9 @@ func cmdUpload(args []string) error { case "-commit": i++ commitSHA = args[i] + case "-campaign-id": + i++ + campaignID = args[i] case "-type": i++ artifactType = args[i] @@ -120,6 +130,12 @@ func cmdUpload(args []string) error { case "-stack-trace": i++ stackTrace = args[i] + case "-kind": + i++ + kind = args[i] + case "-track-key": + i++ + trackKey = args[i] case "-signal": i++ signal = args[i] @@ -144,6 +160,13 @@ func cmdUpload(args []string) error { "commit_sha": commitSHA, "type": artifactType, } + if campaignID != "" { + id, err := strconv.ParseUint(campaignID, 10, 64) + if err != nil { + return fmt.Errorf("invalid campaign id: %w", err) + } + meta["campaign_id"] = id + } if crashMessage != "" { meta["crash_message"] = crashMessage } @@ -151,6 +174,12 @@ func cmdUpload(args []string) error { meta["stack_trace"] = stackTrace } md := map[string]any{} + if kind != "" { + md["kind"] = kind + } + if trackKey != "" { + md["track_key"] = trackKey + } if signal != "" { md["signal"] = signal } diff --git a/internal/handler/ingest.go b/internal/handler/ingest.go index 490aec4..cc391f1 100644 --- a/internal/handler/ingest.go +++ b/internal/handler/ingest.go @@ -25,6 +25,7 @@ type IngestRequest struct { Repository string `json:"repository"` Owner string `json:"owner"` CommitSHA string `json:"commit_sha"` + CampaignID *uint `json:"campaign_id,omitempty"` Type string `json:"type"` CrashMessage string `json:"crash_message,omitempty"` StackTrace string `json:"stack_trace,omitempty"` @@ -93,6 +94,7 @@ func (h *IngestHandler) Create(c *gin.Context) { artifact, err := models.CreateArtifact(ctx, h.DB, models.CreateArtifactParams{ RepositoryID: repo.ID, CommitID: commit.ID, + CampaignID: req.CampaignID, Type: req.Type, BlobKey: blobKey, BlobSize: header.Size,