consolidate the tests locations
This commit is contained in:
@@ -1,48 +0,0 @@
|
||||
id: "c-guarded-system:001"
|
||||
language: c
|
||||
project: guarded-system
|
||||
version: "1.0.0"
|
||||
description: "Command execution guarded by ALLOW_CMD flag (default unreachable)."
|
||||
entrypoints:
|
||||
- "main(argv)"
|
||||
sinks:
|
||||
- id: "GuardedSystem::main"
|
||||
path: "src/main.c::main"
|
||||
kind: "command"
|
||||
location:
|
||||
file: src/main.c
|
||||
line: 26
|
||||
notes: "system() only runs when ALLOW_CMD=1."
|
||||
environment:
|
||||
os_image: "gcc:13-bookworm"
|
||||
runtime:
|
||||
gcc: "13"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "./tests/run-tests.sh"
|
||||
expected_coverage:
|
||||
- outputs/coverage.json
|
||||
expected_traces:
|
||||
- outputs/traces/traces.json
|
||||
ground_truth:
|
||||
summary: "Without ALLOW_CMD, the system() sink remains unreachable; with ALLOW_CMD=1, it executes."
|
||||
evidence_files:
|
||||
- "../../../benchmark/truth/c-guarded-system.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,7 +0,0 @@
|
||||
case_id: "c-guarded-system:001"
|
||||
entries:
|
||||
cli:
|
||||
- id: "main"
|
||||
command: "./app"
|
||||
args: ["<user_input>"]
|
||||
description: "system() guarded by ALLOW_CMD flag"
|
||||
Binary file not shown.
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v0.1",
|
||||
"predicate": {
|
||||
"buildType": "stub",
|
||||
"builder": {
|
||||
"id": "stub"
|
||||
},
|
||||
"metadata": {
|
||||
"buildFinishedOn": "1970-01-01T00:00:00Z",
|
||||
"buildStartedOn": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"predicateType": "https://slsa.dev/provenance/v0.2",
|
||||
"subject": [
|
||||
{
|
||||
"digest": {
|
||||
"sha256": "stub"
|
||||
},
|
||||
"name": "c-guarded-system:001"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "src/main.c",
|
||||
"functions": ["main", "run_guarded"],
|
||||
"coverage": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"components": [],
|
||||
"metadata": {
|
||||
"component": {
|
||||
"name": "guarded-system",
|
||||
"type": "application",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"timestamp": "1970-01-01T00:00:00Z"
|
||||
},
|
||||
"specVersion": "1.5",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"events": [
|
||||
{"path": "src/main.c::main", "type": "entry"},
|
||||
{"path": "src/main.c::run_guarded", "type": "call"}
|
||||
]
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int run_guarded(const char *user_cmd)
|
||||
{
|
||||
const char *allow = getenv("ALLOW_CMD");
|
||||
if (allow == NULL || strcmp(allow, "1") != 0)
|
||||
{
|
||||
puts("command blocked (ALLOW_CMD not set)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof(cmd), "echo START && %s && echo END", user_cmd);
|
||||
return system(cmd);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
fprintf(stderr, "usage: %s <command>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int rc = run_guarded(argv[1]);
|
||||
if (rc != 0)
|
||||
{
|
||||
fprintf(stderr, "command failed\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
puts("done");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUT="${ROOT}/outputs"
|
||||
APP="${OUT}/app"
|
||||
|
||||
if [[ ! -x "${APP}" ]]; then
|
||||
echo "binary missing; run build first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "${tmp}"' EXIT
|
||||
|
||||
# Run without ALLOW_CMD: should be blocked
|
||||
BLOCK_FILE="${tmp}/blocked.txt"
|
||||
ALLOW_CMD=0 "${APP}" "echo SHOULD_NOT_RUN" > "${BLOCK_FILE}"
|
||||
if grep -q "SHOULD_NOT_RUN" "${BLOCK_FILE}"; then
|
||||
echo "command unexpectedly executed when ALLOW_CMD=0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run with ALLOW_CMD set: should execute
|
||||
ALLOW_FILE="${tmp}/allow.txt"
|
||||
ALLOW_CMD=1 "${APP}" "echo ALLOWED" > "${ALLOW_FILE}"
|
||||
if ! grep -q "ALLOWED" "${ALLOW_FILE}"; then
|
||||
echo "command did not execute when ALLOW_CMD=1" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "tests passed"
|
||||
@@ -1,48 +0,0 @@
|
||||
id: "c-memcpy-overflow:001"
|
||||
language: c
|
||||
project: memcpy-overflow
|
||||
version: "1.0.0"
|
||||
description: "Potential overflow: user-controlled length passed to memcpy without bounds."
|
||||
entrypoints:
|
||||
- "process_buffer(len)"
|
||||
sinks:
|
||||
- id: "Overflow::process"
|
||||
path: "src/main.c::process"
|
||||
kind: "memory"
|
||||
location:
|
||||
file: src/main.c
|
||||
line: 23
|
||||
notes: "memcpy uses attacker-controlled length; reachable via process_buffer."
|
||||
environment:
|
||||
os_image: "gcc:13-bookworm"
|
||||
runtime:
|
||||
gcc: "13"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "./tests/run-tests.sh"
|
||||
expected_coverage:
|
||||
- outputs/coverage.json
|
||||
expected_traces:
|
||||
- outputs/traces/traces.json
|
||||
ground_truth:
|
||||
summary: "Calling process_buffer with len>256 drives memcpy with attacker length (reachable)."
|
||||
evidence_files:
|
||||
- "../../../benchmark/truth/c-memcpy-overflow.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,7 +0,0 @@
|
||||
case_id: "c-memcpy-overflow:001"
|
||||
entries:
|
||||
cli:
|
||||
- id: "process_buffer"
|
||||
command: "./app"
|
||||
args: ["<length>"]
|
||||
description: "User length forwarded to memcpy without bounds"
|
||||
Binary file not shown.
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v0.1",
|
||||
"predicate": {
|
||||
"buildType": "stub",
|
||||
"builder": {
|
||||
"id": "stub"
|
||||
},
|
||||
"metadata": {
|
||||
"buildFinishedOn": "1970-01-01T00:00:00Z",
|
||||
"buildStartedOn": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"predicateType": "https://slsa.dev/provenance/v0.2",
|
||||
"subject": [
|
||||
{
|
||||
"digest": {
|
||||
"sha256": "stub"
|
||||
},
|
||||
"name": "c-memcpy-overflow:001"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "src/main.c",
|
||||
"functions": ["main", "process"],
|
||||
"coverage": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"components": [],
|
||||
"metadata": {
|
||||
"component": {
|
||||
"name": "memcpy-overflow",
|
||||
"type": "application",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"timestamp": "1970-01-01T00:00:00Z"
|
||||
},
|
||||
"specVersion": "1.5",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"events": [
|
||||
{"path": "src/main.c::main", "type": "entry"},
|
||||
{"path": "src/main.c::process", "type": "call"}
|
||||
]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int process(size_t len)
|
||||
{
|
||||
char src[512];
|
||||
char dst[128];
|
||||
memset(src, 'A', sizeof(src));
|
||||
memset(dst, 0, sizeof(dst));
|
||||
|
||||
// Attacker-controlled length; no bounds check.
|
||||
memcpy(dst, src, len);
|
||||
|
||||
// Return first byte to keep optimizer from removing the copy.
|
||||
return dst[0];
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
fprintf(stderr, "usage: %s <len>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char *end = NULL;
|
||||
long len = strtol(argv[1], &end, 10);
|
||||
if (end == argv[1] || len < 0)
|
||||
{
|
||||
fprintf(stderr, "invalid length\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int r = process((size_t)len);
|
||||
printf("result=%d\n", r);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUT="${ROOT}/outputs"
|
||||
APP="${OUT}/app"
|
||||
|
||||
if [[ ! -x "${APP}" ]]; then
|
||||
echo "binary missing; run build first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "${tmp}"' EXIT
|
||||
|
||||
# Trigger overflow-prone copy with large length; expect exit code 0
|
||||
RUN_OUT="${tmp}/run.out"
|
||||
"${APP}" "300" > "${RUN_OUT}"
|
||||
|
||||
if ! grep -q "result=" "${RUN_OUT}"; then
|
||||
echo "expected output missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "tests passed"
|
||||
@@ -1,48 +0,0 @@
|
||||
id: "c-unsafe-system:001"
|
||||
language: c
|
||||
project: unsafe-system
|
||||
version: "1.0.0"
|
||||
description: "Command injection sink: user input passed directly to system()."
|
||||
entrypoints:
|
||||
- "main(argv)"
|
||||
sinks:
|
||||
- id: "UnsafeSystem::main"
|
||||
path: "src/main.c::main"
|
||||
kind: "command"
|
||||
location:
|
||||
file: src/main.c
|
||||
line: 21
|
||||
notes: "Untrusted input concatenated into shell command and executed."
|
||||
environment:
|
||||
os_image: "gcc:13-bookworm"
|
||||
runtime:
|
||||
gcc: "13"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "./tests/run-tests.sh"
|
||||
expected_coverage:
|
||||
- outputs/coverage.json
|
||||
expected_traces:
|
||||
- outputs/traces/traces.json
|
||||
ground_truth:
|
||||
summary: "Running with argument 'echo OK' executes system() with user-controlled payload."
|
||||
evidence_files:
|
||||
- "../../../benchmark/truth/c-unsafe-system.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,7 +0,0 @@
|
||||
case_id: "c-unsafe-system:001"
|
||||
entries:
|
||||
cli:
|
||||
- id: "main"
|
||||
command: "./app"
|
||||
args: ["<user_input>"]
|
||||
description: "Passes argv directly into system()"
|
||||
Binary file not shown.
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v0.1",
|
||||
"predicate": {
|
||||
"buildType": "stub",
|
||||
"builder": {
|
||||
"id": "stub"
|
||||
},
|
||||
"metadata": {
|
||||
"buildFinishedOn": "1970-01-01T00:00:00Z",
|
||||
"buildStartedOn": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"predicateType": "https://slsa.dev/provenance/v0.2",
|
||||
"subject": [
|
||||
{
|
||||
"digest": {
|
||||
"sha256": "stub"
|
||||
},
|
||||
"name": "c-unsafe-system:001"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "src/main.c",
|
||||
"functions": ["main", "run_command"],
|
||||
"coverage": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"components": [],
|
||||
"metadata": {
|
||||
"component": {
|
||||
"name": "unsafe-system",
|
||||
"type": "application",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"timestamp": "1970-01-01T00:00:00Z"
|
||||
},
|
||||
"specVersion": "1.5",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"events": [
|
||||
{"path": "src/main.c::main", "type": "entry"},
|
||||
{"path": "src/main.c::run_command", "type": "call"}
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int run_command(const char *user_cmd)
|
||||
{
|
||||
char cmd[256];
|
||||
// Deliberately unsafe: user input embedded directly.
|
||||
snprintf(cmd, sizeof(cmd), "echo START && %s && echo END", user_cmd);
|
||||
return system(cmd);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
fprintf(stderr, "usage: %s <command>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int rc = run_command(argv[1]);
|
||||
if (rc != 0)
|
||||
{
|
||||
fprintf(stderr, "command failed\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
puts("done");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUT="${ROOT}/outputs"
|
||||
APP="${OUT}/app"
|
||||
|
||||
if [[ ! -x "${APP}" ]]; then
|
||||
echo "binary missing; run build first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "${tmp}"' EXIT
|
||||
|
||||
# Run command and capture output deterministically
|
||||
pushd "${tmp}" >/dev/null
|
||||
"${APP}" "echo OK" > "${tmp}/run.out"
|
||||
popd >/dev/null
|
||||
|
||||
if ! grep -q "OK" "${tmp}/run.out"; then
|
||||
echo "expected command output not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "tests passed"
|
||||
@@ -1,46 +0,0 @@
|
||||
id: "go-gin-exec:301"
|
||||
language: go
|
||||
project: gin-exec
|
||||
version: "1.0.0"
|
||||
description: "Command injection sink reachable via GET /run in Gin handler"
|
||||
entrypoints:
|
||||
- "GET /run"
|
||||
sinks:
|
||||
- id: "CommandInjection::handleRun"
|
||||
path: "main.handleRun"
|
||||
kind: "custom"
|
||||
location:
|
||||
file: main.go
|
||||
line: 22
|
||||
notes: "os/exec.Command with user-controlled input"
|
||||
environment:
|
||||
os_image: "golang:1.22-alpine"
|
||||
runtime:
|
||||
go: "1.22"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "2Gi"
|
||||
build:
|
||||
command: "go build -o outputs/app ."
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/app
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "go test -v ./..."
|
||||
expected_coverage: []
|
||||
expected_traces: []
|
||||
ground_truth:
|
||||
summary: "Command injection reachable"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/go-gin-exec.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,8 +0,0 @@
|
||||
case_id: "go-gin-exec:301"
|
||||
entries:
|
||||
http:
|
||||
- id: "GET /run"
|
||||
route: "/run"
|
||||
method: "GET"
|
||||
handler: "main.handleRun"
|
||||
description: "Executes shell command from query parameter"
|
||||
@@ -1,5 +0,0 @@
|
||||
module gin-exec
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/gin-gonic/gin v1.10.0
|
||||
@@ -1,41 +0,0 @@
|
||||
// gin-exec benchmark case
|
||||
// Demonstrates command injection sink reachable via Gin HTTP handler
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.GET("/run", handleRun)
|
||||
r.GET("/health", handleHealth)
|
||||
r.Run(":8080")
|
||||
}
|
||||
|
||||
// handleRun - VULNERABLE: command injection sink
|
||||
// User-controlled input passed directly to exec.Command
|
||||
func handleRun(c *gin.Context) {
|
||||
cmd := c.Query("cmd")
|
||||
if cmd == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing cmd parameter"})
|
||||
return
|
||||
}
|
||||
|
||||
// SINK: os/exec.Command with user-controlled input
|
||||
output, err := exec.Command("sh", "-c", cmd).Output()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"output": string(output)})
|
||||
}
|
||||
|
||||
// handleHealth - safe endpoint, no sinks
|
||||
func handleHealth(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestHandleHealth(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.Default()
|
||||
r.GET("/health", handleHealth)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/health", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRunMissingCmd(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.Default()
|
||||
r.GET("/run", handleRun)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/run", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# Keep this directory for build outputs
|
||||
@@ -1,46 +0,0 @@
|
||||
id: "go-grpc-sql:302"
|
||||
language: go
|
||||
project: grpc-sql
|
||||
version: "1.0.0"
|
||||
description: "SQL injection sink reachable via gRPC GetUser method"
|
||||
entrypoints:
|
||||
- "grpc:UserService.GetUser"
|
||||
sinks:
|
||||
- id: "SqlInjection::GetUser"
|
||||
path: "main.(*userServer).GetUser"
|
||||
kind: "custom"
|
||||
location:
|
||||
file: main.go
|
||||
line: 35
|
||||
notes: "database/sql.Query with string concatenation"
|
||||
environment:
|
||||
os_image: "golang:1.22-alpine"
|
||||
runtime:
|
||||
go: "1.22"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "2Gi"
|
||||
build:
|
||||
command: "go build -o outputs/app ."
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/app
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "go test -v ./..."
|
||||
expected_coverage: []
|
||||
expected_traces: []
|
||||
ground_truth:
|
||||
summary: "SQL injection reachable"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/go-grpc-sql.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,8 +0,0 @@
|
||||
case_id: "go-grpc-sql:302"
|
||||
entries:
|
||||
grpc:
|
||||
- id: "grpc:UserService.GetUser"
|
||||
service: "UserService"
|
||||
method: "GetUser"
|
||||
handler: "main.(*userServer).GetUser"
|
||||
description: "Fetches user by ID with SQL injection vulnerability"
|
||||
@@ -1,8 +0,0 @@
|
||||
module grpc-sql
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
google.golang.org/grpc v1.64.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
)
|
||||
@@ -1,86 +0,0 @@
|
||||
// grpc-sql benchmark case
|
||||
// Demonstrates SQL injection sink reachable via gRPC handler
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// User represents a user record
|
||||
type User struct {
|
||||
ID string
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
// userServer implements the gRPC UserService
|
||||
type userServer struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// GetUser - VULNERABLE: SQL injection sink
|
||||
// User ID is concatenated directly into SQL query
|
||||
func (s *userServer) GetUser(ctx context.Context, userID string) (*User, error) {
|
||||
// SINK: database/sql.Query with string concatenation
|
||||
query := fmt.Sprintf("SELECT id, name, email FROM users WHERE id = '%s'", userID)
|
||||
row := s.db.QueryRow(query)
|
||||
|
||||
var user User
|
||||
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserSafe - SAFE: uses parameterized query
|
||||
func (s *userServer) GetUserSafe(ctx context.Context, userID string) (*User, error) {
|
||||
query := "SELECT id, name, email FROM users WHERE id = ?"
|
||||
row := s.db.QueryRow(query, userID)
|
||||
|
||||
var user User
|
||||
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Initialize schema
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
email TEXT
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create table: %v", err)
|
||||
}
|
||||
|
||||
lis, err := net.Listen("tcp", ":50051")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
|
||||
s := grpc.NewServer()
|
||||
// Register service here (simplified for benchmark)
|
||||
log.Printf("gRPC server listening on %v", lis.Addr())
|
||||
if err := s.Serve(lis); err != nil {
|
||||
log.Fatalf("failed to serve: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open database: %v", err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
email TEXT
|
||||
);
|
||||
INSERT INTO users (id, name, email) VALUES ('1', 'Alice', 'alice@example.com');
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to setup test data: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func TestGetUserSafe(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
server := &userServer{db: db}
|
||||
user, err := server.GetUserSafe(context.Background(), "1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if user.Name != "Alice" {
|
||||
t.Errorf("expected Alice, got %s", user.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserNotFound(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
server := &userServer{db: db}
|
||||
_, err := server.GetUserSafe(context.Background(), "999")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent user")
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# Keep this directory for build outputs
|
||||
@@ -1,48 +0,0 @@
|
||||
id: "java-micronaut-deserialize:203"
|
||||
language: java
|
||||
project: micronaut-deserialize
|
||||
version: "1.0.0"
|
||||
description: "Micronaut-style controller performs unsafe deserialization on request payload"
|
||||
entrypoints:
|
||||
- "POST /mn/upload"
|
||||
sinks:
|
||||
- id: "MicronautDeserialize::handleUpload"
|
||||
path: "bench.reachability.micronaut.Controller.handleUpload"
|
||||
kind: "custom"
|
||||
location:
|
||||
file: src/Controller.java
|
||||
line: 10
|
||||
notes: "ObjectInputStream on user-controlled payload"
|
||||
environment:
|
||||
os_image: "eclipse-temurin:21-jdk"
|
||||
runtime:
|
||||
java: "21"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "./build/build.sh"
|
||||
expected_coverage: []
|
||||
expected_traces: []
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: "-ea"
|
||||
ground_truth:
|
||||
summary: "Deserialization reachable"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/java-micronaut-deserialize.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,8 +0,0 @@
|
||||
case_id: "java-micronaut-deserialize:203"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /mn/upload"
|
||||
route: "/mn/upload"
|
||||
method: "POST"
|
||||
handler: "Controller.handleUpload"
|
||||
description: "Binary payload base64-deserialized"
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v0.1",
|
||||
"predicate": {
|
||||
"buildType": "stub",
|
||||
"builder": {
|
||||
"id": "stub"
|
||||
},
|
||||
"metadata": {
|
||||
"buildFinishedOn": "1970-01-01T00:00:00Z",
|
||||
"buildStartedOn": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"predicateType": "https://slsa.dev/provenance/v0.2",
|
||||
"subject": [
|
||||
{
|
||||
"digest": {
|
||||
"sha256": "stub"
|
||||
},
|
||||
"name": "java-micronaut-deserialize:203"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
true
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"files": {
|
||||
"src/Controller.java": {
|
||||
"lines_covered": [
|
||||
11,
|
||||
14,
|
||||
15,
|
||||
17
|
||||
],
|
||||
"lines_total": 40
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"components": [],
|
||||
"metadata": {
|
||||
"component": {
|
||||
"name": "micronaut-deserialize",
|
||||
"type": "application",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"timestamp": "1970-01-01T00:00:00Z"
|
||||
},
|
||||
"specVersion": "1.5",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"entry": "POST /mn/upload",
|
||||
"notes": "Base64 payload flows into ObjectInputStream without guard",
|
||||
"path": [
|
||||
"Controller.handleUpload",
|
||||
"ObjectInputStream.readObject"
|
||||
],
|
||||
"sink": "MicronautDeserialize::handleUpload"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.stellaops.bench</groupId>
|
||||
<artifactId>micronaut-deserialize</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
</project>
|
||||
@@ -1,24 +0,0 @@
|
||||
package bench.reachability.micronaut;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Base64;
|
||||
import java.io.*;
|
||||
|
||||
public class Controller {
|
||||
// Unsafe deserialization sink (reachable)
|
||||
public static Response handleUpload(Map<String, String> body) {
|
||||
String payload = body.get("payload");
|
||||
if (payload == null) {
|
||||
return new Response(400, "bad request");
|
||||
}
|
||||
try (ObjectInputStream ois = new ObjectInputStream(
|
||||
new ByteArrayInputStream(Base64.getDecoder().decode(payload)))) {
|
||||
Object obj = ois.readObject();
|
||||
return new Response(200, obj.toString());
|
||||
} catch (Exception ex) {
|
||||
return new Response(500, ex.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
public record Response(int status, String body) {}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package bench.reachability.micronaut;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.Base64;
|
||||
|
||||
// Simple assertion-based oracle (JUnit-free for offline determinism)
|
||||
public class ControllerTest {
|
||||
private static String serialize(Object obj) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
|
||||
oos.writeObject(obj);
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(bos.toByteArray());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Map<String, String> body = Map.of("payload", serialize("micronaut"));
|
||||
var res = Controller.handleUpload(body);
|
||||
assert res.status() == 200 : "status";
|
||||
assert res.body().equals("micronaut") : "body";
|
||||
|
||||
File outDir = new File("outputs");
|
||||
outDir.mkdirs();
|
||||
try (FileWriter fw = new FileWriter(new File(outDir, "SINK_REACHED"))) {
|
||||
fw.write("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
id: "java-micronaut-guarded:204"
|
||||
language: java
|
||||
project: micronaut-guarded
|
||||
version: "1.0.0"
|
||||
description: "Micronaut-style controller guards deserialization behind ALLOW_MN_DESER flag (unreachable by default)"
|
||||
entrypoints:
|
||||
- "POST /mn/upload"
|
||||
sinks:
|
||||
- id: "MicronautDeserializeGuarded::handleUpload"
|
||||
path: "bench.reachability.micronautguard.Controller.handleUpload"
|
||||
kind: "custom"
|
||||
location:
|
||||
file: src/Controller.java
|
||||
line: 11
|
||||
notes: "ObjectInputStream gated by ALLOW_MN_DESER"
|
||||
environment:
|
||||
os_image: "eclipse-temurin:21-jdk"
|
||||
runtime:
|
||||
java: "21"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "./build/build.sh"
|
||||
expected_coverage: []
|
||||
expected_traces: []
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: "-ea"
|
||||
ground_truth:
|
||||
summary: "Guard blocks deserialization unless ALLOW_MN_DESER=true"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/java-micronaut-guarded.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,8 +0,0 @@
|
||||
case_id: "java-micronaut-guarded:204"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /mn/upload"
|
||||
route: "/mn/upload"
|
||||
method: "POST"
|
||||
handler: "Controller.handleUpload"
|
||||
description: "Deserialization guarded by ALLOW_MN_DESER flag"
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v0.1",
|
||||
"predicate": {
|
||||
"buildType": "stub",
|
||||
"builder": {
|
||||
"id": "stub"
|
||||
},
|
||||
"metadata": {
|
||||
"buildFinishedOn": "1970-01-01T00:00:00Z",
|
||||
"buildStartedOn": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"predicateType": "https://slsa.dev/provenance/v0.2",
|
||||
"subject": [
|
||||
{
|
||||
"digest": {
|
||||
"sha256": "stub"
|
||||
},
|
||||
"name": "java-micronaut-guarded:204"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
true
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"files": {
|
||||
"src/Controller.java": {
|
||||
"lines_covered": [
|
||||
12,
|
||||
13,
|
||||
15,
|
||||
17
|
||||
],
|
||||
"lines_total": 42
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"components": [],
|
||||
"metadata": {
|
||||
"component": {
|
||||
"name": "micronaut-guarded",
|
||||
"type": "application",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"timestamp": "1970-01-01T00:00:00Z"
|
||||
},
|
||||
"specVersion": "1.5",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"entry": "POST /mn/upload",
|
||||
"notes": "Guard enforces ALLOW_MN_DESER feature flag; sink not reached by default",
|
||||
"path": [
|
||||
"Controller.handleUpload"
|
||||
],
|
||||
"sink": "MicronautDeserializeGuarded::handleUpload"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.stellaops.bench</groupId>
|
||||
<artifactId>micronaut-guarded</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
</project>
|
||||
@@ -1,27 +0,0 @@
|
||||
package bench.reachability.micronautguard;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Base64;
|
||||
import java.io.*;
|
||||
|
||||
public class Controller {
|
||||
// Deserialization behind feature flag; unreachable unless ALLOW_MN_DESER=true
|
||||
public static Response handleUpload(Map<String, String> body, Map<String, String> env) {
|
||||
if (!"true".equals(env.getOrDefault("ALLOW_MN_DESER", "false"))) {
|
||||
return new Response(403, "forbidden");
|
||||
}
|
||||
String payload = body.get("payload");
|
||||
if (payload == null) {
|
||||
return new Response(400, "bad request");
|
||||
}
|
||||
try (ObjectInputStream ois = new ObjectInputStream(
|
||||
new ByteArrayInputStream(Base64.getDecoder().decode(payload)))) {
|
||||
Object obj = ois.readObject();
|
||||
return new Response(200, obj.toString());
|
||||
} catch (Exception ex) {
|
||||
return new Response(500, ex.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
public record Response(int status, String body) {}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package bench.reachability.micronautguard;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.Base64;
|
||||
|
||||
public class ControllerTest {
|
||||
private static String serialize(Object obj) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
|
||||
oos.writeObject(obj);
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(bos.toByteArray());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Map<String, String> body = Map.of("payload", serialize("blocked"));
|
||||
Map<String, String> env = Map.of("ALLOW_MN_DESER", "false");
|
||||
var res = Controller.handleUpload(body, env);
|
||||
assert res.status() == 403 : "status";
|
||||
assert res.body().equals("forbidden") : "body";
|
||||
|
||||
File outDir = new File("outputs");
|
||||
outDir.mkdirs();
|
||||
try (FileWriter fw = new FileWriter(new File(outDir, "SINK_BLOCKED"))) {
|
||||
fw.write("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
id: "java-spring-deserialize:201"
|
||||
language: java
|
||||
project: spring-deserialize
|
||||
version: "1.0.0"
|
||||
description: "Java deserialization sink reachable via POST /api/upload"
|
||||
entrypoints:
|
||||
- "POST /api/upload"
|
||||
sinks:
|
||||
- id: "JavaDeserialize::handleRequest"
|
||||
path: "bench.reachability.App.handleRequest"
|
||||
kind: "custom"
|
||||
location:
|
||||
file: src/App.java
|
||||
line: 9
|
||||
notes: "java.io.ObjectInputStream on user-controlled payload"
|
||||
environment:
|
||||
os_image: "eclipse-temurin:21-jdk"
|
||||
runtime:
|
||||
java: "21"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "./build/build.sh"
|
||||
expected_coverage: []
|
||||
expected_traces: []
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: "-ea"
|
||||
ground_truth:
|
||||
summary: "Deserialization reachable"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/java-spring-deserialize.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,8 +0,0 @@
|
||||
case_id: "java-spring-deserialize:201"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /api/upload"
|
||||
route: "/api/upload"
|
||||
method: "POST"
|
||||
handler: "App.handleRequest"
|
||||
description: "Binary payload base64-deserialized"
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v0.1",
|
||||
"predicate": {
|
||||
"buildType": "stub",
|
||||
"builder": {
|
||||
"id": "stub"
|
||||
},
|
||||
"metadata": {
|
||||
"buildFinishedOn": "1970-01-01T00:00:00Z",
|
||||
"buildStartedOn": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"predicateType": "https://slsa.dev/provenance/v0.2",
|
||||
"subject": [
|
||||
{
|
||||
"digest": {
|
||||
"sha256": "stub"
|
||||
},
|
||||
"name": "java-spring-deserialize:201"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
true
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"files": {
|
||||
"src/App.java": {
|
||||
"lines_covered": [
|
||||
9,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
19
|
||||
],
|
||||
"lines_total": 26
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"components": [],
|
||||
"metadata": {
|
||||
"component": {
|
||||
"name": "spring-deserialize",
|
||||
"type": "application",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"timestamp": "1970-01-01T00:00:00Z"
|
||||
},
|
||||
"specVersion": "1.5",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"entry": "POST /api/upload",
|
||||
"notes": "No guard; base64 payload deserialized",
|
||||
"path": [
|
||||
"App.handleRequest",
|
||||
"ObjectInputStream.readObject"
|
||||
],
|
||||
"sink": "JavaDeserialize::handleRequest"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.stellaops.bench</groupId>
|
||||
<artifactId>spring-deserialize</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
</project>
|
||||
@@ -1,26 +0,0 @@
|
||||
package bench.reachability;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Base64;
|
||||
import java.io.*;
|
||||
|
||||
public class App {
|
||||
// Unsafe Java deserialization sink (reachable)
|
||||
public static Response handleRequest(Map<String, String> body) {
|
||||
String payload = body.get("payload");
|
||||
if (payload == null) {
|
||||
return new Response(400, "bad request");
|
||||
}
|
||||
try {
|
||||
byte[] data = Base64.getDecoder().decode(payload);
|
||||
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
|
||||
Object obj = ois.readObject();
|
||||
ois.close();
|
||||
return new Response(200, obj.toString());
|
||||
} catch (Exception ex) {
|
||||
return new Response(500, ex.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
public record Response(int status, String body) {}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package bench.reachability;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.Base64;
|
||||
|
||||
// Simple hand-rolled test harness (no external deps) using Java assertions.
|
||||
public class AppTest {
|
||||
private static String serialize(Object obj) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(bos);
|
||||
oos.writeObject(obj);
|
||||
oos.close();
|
||||
return Base64.getEncoder().encodeToString(bos.toByteArray());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String payload = serialize("hello");
|
||||
Map<String, String> body = Map.of("payload", payload);
|
||||
var res = App.handleRequest(body);
|
||||
assert res.status() == 200 : "status";
|
||||
assert res.body().equals("hello") : "body";
|
||||
// Emit a simple marker file for trace/coverage stand-ins
|
||||
File outDir = new File("outputs");
|
||||
outDir.mkdirs();
|
||||
try (FileWriter fw = new FileWriter(new File(outDir, "SINK_REACHED"))) {
|
||||
fw.write("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
id: "java-spring-guarded:202"
|
||||
language: java
|
||||
project: spring-guarded
|
||||
version: "1.0.0"
|
||||
description: "Java deserialization guarded by ALLOW_DESER flag (unreachable by default)"
|
||||
entrypoints:
|
||||
- "POST /api/upload"
|
||||
sinks:
|
||||
- id: "JavaDeserializeGuarded::handleRequest"
|
||||
path: "bench.reachability.App.handleRequest"
|
||||
kind: "custom"
|
||||
location:
|
||||
file: src/App.java
|
||||
line: 9
|
||||
notes: "ObjectInputStream gated by ALLOW_DESER"
|
||||
environment:
|
||||
os_image: "eclipse-temurin:21-jdk"
|
||||
runtime:
|
||||
java: "21"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "./build/build.sh"
|
||||
expected_coverage: []
|
||||
expected_traces: []
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: "-ea"
|
||||
ground_truth:
|
||||
summary: "Guard blocks deserialization unless ALLOW_DESER=true"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/java-spring-guarded.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,8 +0,0 @@
|
||||
case_id: "java-spring-guarded:202"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /api/upload"
|
||||
route: "/api/upload"
|
||||
method: "POST"
|
||||
handler: "App.handleRequest"
|
||||
description: "Base64 payload deserialization guarded by ALLOW_DESER"
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v0.1",
|
||||
"predicate": {
|
||||
"buildType": "stub",
|
||||
"builder": {
|
||||
"id": "stub"
|
||||
},
|
||||
"metadata": {
|
||||
"buildFinishedOn": "1970-01-01T00:00:00Z",
|
||||
"buildStartedOn": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
"predicateType": "https://slsa.dev/provenance/v0.2",
|
||||
"subject": [
|
||||
{
|
||||
"digest": {
|
||||
"sha256": "stub"
|
||||
},
|
||||
"name": "java-spring-guarded:202"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
true
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"files": {
|
||||
"src/App.java": {
|
||||
"lines_covered": [
|
||||
10,
|
||||
11,
|
||||
13,
|
||||
15
|
||||
],
|
||||
"lines_total": 29
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"components": [],
|
||||
"metadata": {
|
||||
"component": {
|
||||
"name": "spring-guarded",
|
||||
"type": "application",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"timestamp": "1970-01-01T00:00:00Z"
|
||||
},
|
||||
"specVersion": "1.5",
|
||||
"version": 1
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"entry": "POST /api/upload",
|
||||
"notes": "Guard requires ALLOW_DESER=true; sink not executed by default",
|
||||
"path": [
|
||||
"App.handleRequest"
|
||||
],
|
||||
"sink": "JavaDeserializeGuarded::handleRequest"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.stellaops.bench</groupId>
|
||||
<artifactId>spring-guarded</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
</project>
|
||||
@@ -1,29 +0,0 @@
|
||||
package bench.reachability;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Base64;
|
||||
import java.io.*;
|
||||
|
||||
public class App {
|
||||
// Deserialization sink guarded by feature flag
|
||||
public static Response handleRequest(Map<String, String> body, Map<String, String> env) {
|
||||
if (!"true".equals(env.getOrDefault("ALLOW_DESER", "false"))) {
|
||||
return new Response(403, "forbidden");
|
||||
}
|
||||
String payload = body.get("payload");
|
||||
if (payload == null) {
|
||||
return new Response(400, "bad request");
|
||||
}
|
||||
try {
|
||||
byte[] data = Base64.getDecoder().decode(payload);
|
||||
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
|
||||
Object obj = ois.readObject();
|
||||
ois.close();
|
||||
return new Response(200, obj.toString());
|
||||
} catch (Exception ex) {
|
||||
return new Response(500, ex.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
public record Response(int status, String body) {}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package bench.reachability;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.Base64;
|
||||
|
||||
public class AppTest {
|
||||
private static String serialize(Object obj) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(bos);
|
||||
oos.writeObject(obj);
|
||||
oos.close();
|
||||
return Base64.getEncoder().encodeToString(bos.toByteArray());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String payload = serialize("hi");
|
||||
Map<String, String> body = Map.of("payload", payload);
|
||||
Map<String, String> env = Map.of("ALLOW_DESER", "false");
|
||||
var res = App.handleRequest(body, env);
|
||||
assert res.status() == 403 : "status";
|
||||
assert res.body().equals("forbidden") : "body";
|
||||
File outDir = new File("outputs");
|
||||
outDir.mkdirs();
|
||||
try (FileWriter fw = new FileWriter(new File(outDir, "SINK_BLOCKED"))) {
|
||||
fw.write("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
id: "java-spring-reflection:205"
|
||||
language: java
|
||||
project: spring-reflection
|
||||
version: "1.0.0"
|
||||
description: "Spring-style controller exposes reflection endpoint that loads arbitrary classes"
|
||||
entrypoints:
|
||||
- "POST /api/reflect"
|
||||
sinks:
|
||||
- id: "SpringReflection::run"
|
||||
path: "bench.reachability.springreflection.ReflectController.run"
|
||||
kind: "custom"
|
||||
location:
|
||||
file: src/ReflectController.java
|
||||
line: 7
|
||||
notes: "User-controlled Class.forName + newInstance"
|
||||
environment:
|
||||
os_image: "eclipse-temurin:21-jdk"
|
||||
runtime:
|
||||
java: "21"
|
||||
source_date_epoch: 1730000000
|
||||
resource_limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
build:
|
||||
command: "./build/build.sh"
|
||||
source_date_epoch: 1730000000
|
||||
outputs:
|
||||
artifact_path: outputs/binary.tar.gz
|
||||
sbom_path: outputs/sbom.cdx.json
|
||||
coverage_path: outputs/coverage.json
|
||||
traces_dir: outputs/traces
|
||||
attestation_path: outputs/attestation.json
|
||||
test:
|
||||
command: "./build/build.sh"
|
||||
expected_coverage: []
|
||||
expected_traces: []
|
||||
env:
|
||||
JAVA_TOOL_OPTIONS: "-ea"
|
||||
ground_truth:
|
||||
summary: "Reflection sink reachable with user-controlled class name"
|
||||
evidence_files:
|
||||
- "../benchmark/truth/java-spring-reflection.json"
|
||||
sandbox:
|
||||
network: loopback
|
||||
privileges: rootless
|
||||
redaction:
|
||||
pii: false
|
||||
policy: "benchmark-default/v1"
|
||||
@@ -1,8 +0,0 @@
|
||||
case_id: "java-spring-reflection:205"
|
||||
entries:
|
||||
http:
|
||||
- id: "POST /api/reflect"
|
||||
route: "/api/reflect"
|
||||
method: "POST"
|
||||
handler: "ReflectController.run"
|
||||
description: "Reflection endpoint loads arbitrary classes"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user