Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions cmd/room-semgrep/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"testing"
"time"
Expand Down Expand Up @@ -496,14 +497,22 @@ func workspace(t *testing.T) (string, string) {
return root, repository
}

// toolDigests caches tool-binary hashes so integration tests do not re-read
// the 200 MiB semgrep-core binary for every request.
var toolDigests sync.Map

func requestFor(repository, config, semgrepCore string, content []byte) analyzerRequest {
request := requestForPhase(repository, content, "ANALYSIS_PHASE_DIFF")
configData, _ := os.ReadFile(config)
configDigest := sha256.Sum256(configData)
request.ConfigSHA256 = hex.EncodeToString(configDigest[:])
toolData, _ := os.ReadFile(semgrepCore)
toolDigest := sha256.Sum256(toolData)
request.ToolSHA256 = hex.EncodeToString(toolDigest[:])
digest, ok := toolDigests.Load(semgrepCore)
if !ok {
toolData, _ := os.ReadFile(semgrepCore)
toolDigest := sha256.Sum256(toolData)
digest, _ = toolDigests.LoadOrStore(semgrepCore, hex.EncodeToString(toolDigest[:]))
}
request.ToolSHA256 = digest.(string)
return request
}

Expand Down
14 changes: 12 additions & 2 deletions docs/analyzer.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ renames, binary patches, and malformed or incomplete unified diffs produce a
failed receipt. The adapter invokes `semgrep-core` with explicit snapshotted
targets, strict rule validation, disabled timeouts, and no ignore-file target
discovery. Any core parser error, target skip, or rule skip fails the receipt.
The scan runs in its own process group, so canceling the analysis kills
`semgrep-core` and anything it spawned.
Registry configuration URLs are not accepted.

Each policy-bearing Semgrep rule must provide these metadata fields:
Expand Down Expand Up @@ -101,11 +103,19 @@ The bundled rules provide these coverage claims:
| `SIGNAL_KIND_RUST_BLOCKING_LOCK_ACROSS_AWAIT` | Rust | `lock`/`read`/`write` bindings using `unwrap` or `expect`, and `blocking_lock`/`blocking_read`/`blocking_write` bindings, followed by await without a matched explicit drop |

These rules model the listed source and sink families, not every framework API
or validation function. The adapter uses Semgrep's private core interface and
is pinned and integration-tested against `semgrep-core` 1.139.0. For taint
or validation function. Semgrep CE tracks taint within a single file, so a
flow that crosses a file boundary produces no finding. The adapter uses
Semgrep's private core interface and is pinned and integration-tested against
`semgrep-core` 1.139.0. For taint
rules, a finding is retained when the added lines intersect either the reported
sink or a source/intermediate location in the core's dataflow trace.

Upgrading `semgrep-core` means: bump `semgrepCoreVersion` in
`cmd/room-semgrep/main.go`, the `semgrep==` pin, and the `-version` assertion
in `.github/workflows/ci.yml`, then rerun the integration suite. A version
mismatch or a change to the core's private target-manifest or dataflow-trace
schema fails closed.

The bundled Semgrep rules do not claim
`SIGNAL_KIND_RUST_UNSAFE_WITHOUT_SAFETY_CONTRACT`,
`SIGNAL_KIND_RUST_PANIC_IN_LIBRARY_API`, or
Expand Down
28 changes: 28 additions & 0 deletions internal/analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -249,6 +250,33 @@ func TestExternalAnalyzerUsesJSONStdinAndLiteralArguments(t *testing.T) {
}
}

func TestExternalAnalyzerOmitsToolDigestWithoutTool(t *testing.T) {
input := Input{Phase: roomv1.AnalysisPhase_ANALYSIS_PHASE_PLAN, Content: []byte("plan")}
digest := sha256.Sum256(input.Content)
temp := t.TempDir()
capture := filepath.Join(temp, "request.json")
response := fmt.Sprintf(`{"phase":"ANALYSIS_PHASE_PLAN","status":"ANALYSIS_STATUS_COMPLETE","covered_signals":["SIGNAL_KIND_SECRET_LITERAL"],"input_sha256":%q}`, hex.EncodeToString(digest[:]))
executable := filepath.Join(temp, "capture-provider")
script := fmt.Sprintf("#!/bin/sh\ncat > '%s'\nprintf '%%s' '%s'\n", capture, response)
if err := os.WriteFile(executable, []byte(script), 0o700); err != nil {
t.Fatal(err)
}
a, err := NewExternal(Config{ID: "plain", Version: "1", Executable: executable, CoveredSignals: []roomv1.SignalKind{secretSignal}})
if err != nil {
t.Fatal(err)
}
if got := a.Analyze(context.Background(), input).GetStatus(); got != roomv1.AnalysisStatus_ANALYSIS_STATUS_COMPLETE {
t.Fatalf("status = %s", got)
}
captured, err := os.ReadFile(capture)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(captured), "tool_sha256") {
t.Fatalf("request named a tool digest without a configured tool: %s", captured)
}
}

func TestNewExternalRejectsUnsafeOrIncompleteConfiguration(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading