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
2 changes: 1 addition & 1 deletion .github/agents/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ You are **Aegis**, a Security Architect and Golang Sentinel. Your job is to fort

## Critical Developer Workflows

- **Install:** `go install github.com/BlackVectorOps/semantic_firewall/v3/cmd/sfw@latest`
- **Install:** `go install github.com/BlackVectorOps/semantic_firewall/v4/cmd/sfw@latest`
- **Check file:** `sfw check ./main.go`
- **Semantic diff:** `sfw diff old.go new.go`
- **Index malware:** `sfw index malware.go --name "Beacon_v1" --severity CRITICAL`
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

### Next-Gen Code Integrity & Malware Detection for Go

[![Go Reference](https://pkg.go.dev/badge/github.com/BlackVectorOps/semantic_firewall.svg)](https://pkg.go.dev/github.com/BlackVectorOps/semantic_firewall/v3)
[![Go Reference](https://pkg.go.dev/badge/github.com/BlackVectorOps/semantic_firewall.svg)](https://pkg.go.dev/github.com/BlackVectorOps/semantic_firewall/v4)
[![License: MIT](https://img.shields.io/badge/License-MIT-00d4aa.svg)](LICENSE)
[![Marketplace](https://img.shields.io/badge/Marketplace-Semantic_Firewall-7c3aed.svg)](https://github.com/marketplace/actions/semantic-firewall)
[![Semantic Check](https://github.com/BlackVectorOps/semantic_firewall/actions/workflows/semantic-check.yml/badge.svg)](https://github.com/BlackVectorOps/semantic_firewall/actions/workflows/semantic-check.yml)
Expand Down Expand Up @@ -68,7 +68,7 @@
## Getting Started

```bash
go install github.com/BlackVectorOps/semantic_firewall/v3/cmd/sfw@latest
go install github.com/BlackVectorOps/semantic_firewall/v4/cmd/sfw@latest
```


Expand Down Expand Up @@ -270,7 +270,7 @@ jobs:
- uses: actions/checkout@v3
- name: Run Semantic Firewall
run: |
go install github.com/BlackVectorOps/semantic_firewall/v3/cmd/sfw@latest
go install github.com/BlackVectorOps/semantic_firewall/v4/cmd/sfw@latest
sfw diff old.go new.go
sfw scan . --deps
```
Expand Down Expand Up @@ -731,7 +731,7 @@ Functions are matched by their **structural fingerprint** (block count, call pro
### Fingerprinting

```go
import semanticfw "github.com/BlackVectorOps/semantic_firewall/v3"
import semanticfw "github.com/BlackVectorOps/semantic_firewall/v4"

src := `package main
func Add(a, b int) int { return a + b }
Expand All @@ -750,7 +750,7 @@ for _, r := range results {
### Malware Scanning with PebbleDB

```go
import semanticfw "github.com/BlackVectorOps/semantic_firewall/v3"
import semanticfw "github.com/BlackVectorOps/semantic_firewall/v4"

// Open the signature database
scanner, err := semanticfw.NewPebbleScanner("signatures.db", semanticfw.DefaultPebbleScannerOptions())
Expand Down Expand Up @@ -778,7 +778,7 @@ for _, alert := range alerts {
### Topology Extraction

```go
import semanticfw "github.com/BlackVectorOps/semantic_firewall/v3"
import semanticfw "github.com/BlackVectorOps/semantic_firewall/v4"

// Extract structural features from an SSA function
topo := semanticfw.ExtractTopology(ssaFunction)
Expand Down
11 changes: 11 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,17 @@ runs:
;;

audit)
# sfw resolves the audit API key from OPENAI_API_KEY or
# GEMINI_API_KEY (selected by the model name) -- it never reads
# SFW_API_KEY. Re-export the supplied key under the name the CLI
# actually consumes, otherwise audit always fails "API Key required".
if [[ -n "${SFW_API_KEY:-}" ]]; then
if [[ "${INPUT_MODEL,,}" == gemini* ]]; then
export GEMINI_API_KEY="$SFW_API_KEY"
else
export OPENAI_API_KEY="$SFW_API_KEY"
fi
fi
readonly WORKTREE_DIR="${PREP_WORKTREE_DIR:-.sfw_base_worktree}"
readonly DIFF_STREAM_FILE=".sfw_diff_stream.bin"

Expand Down
26 changes: 10 additions & 16 deletions cmd/sfw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"os"
"strings"

"github.com/BlackVectorOps/semantic_firewall/v3/internal/cli"
"github.com/BlackVectorOps/semantic_firewall/v3/pkg/models"
version "github.com/BlackVectorOps/semantic_firewall/v3/pkg/version"
"github.com/BlackVectorOps/semantic_firewall/v4/internal/cli"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/models"
version "github.com/BlackVectorOps/semantic_firewall/v4/pkg/version"
)

// Package main provides the sfw CLI tool for semantic fingerprinting and malware scanning of Go source files.
Expand Down Expand Up @@ -53,6 +53,7 @@ Commands:
--api-key API Key (OpenAI or Gemini). REQUIRED.
--model LLM Model (default: gpt-4o, supports gemini-1.5-pro)
--api-base Custom API Base URL (for testing/proxying)
--no-sandbox Disable gVisor/Namespace isolation

index Index a reference malware sample (Lab Phase)
scan Scan target code for malware signatures (Hunter Phase)
Expand Down Expand Up @@ -92,6 +93,7 @@ Examples:
// Default updated to gpt-4o per 2026 standards (Reasoning Optimized)
auditModel := auditCmd.String("model", "gpt-4o", "LLM Model to use")
auditApiBase := auditCmd.String("api-base", "", "Custom API Base URL")
auditNoSandbox := auditCmd.Bool("no-sandbox", false, "Disable gVisor/Namespace isolation")

indexCmd := flag.NewFlagSet("index", flag.ExitOnError)
indexName := indexCmd.String("name", "", "Signature name (required)")
Expand Down Expand Up @@ -166,7 +168,7 @@ Examples:
os.Exit(1)
}

exitCode, err := cli.RunAudit(os.Stdout, auditCmd.Arg(0), auditCmd.Arg(1), auditCmd.Arg(2), apiKey, *auditModel, *auditApiBase)
exitCode, err := cli.RunAudit(os.Stdout, auditCmd.Arg(0), auditCmd.Arg(1), auditCmd.Arg(2), apiKey, *auditModel, *auditApiBase, *auditNoSandbox)
if err != nil {
cli.ExitError(err)
}
Expand Down Expand Up @@ -269,19 +271,11 @@ func runWorker(args []string) error {
return cli.RunCheckLogic(fsys, *target, *strict, *scan, resolvedDB)

case "diff":
// FIXED: Support both standard 3-arg usage [diff, old, new] and legacy 5-arg usage
// 3-arg usage: sfw internal-worker diff <old> <new>
if len(args) == 3 {
return cli.RunDiffLogic(fsys, args[1], args[2])
// sfw internal-worker diff <old> <new>
if len(args) != 3 {
return fmt.Errorf("diff worker requires arguments (old <path> new <path>); got %d", len(args)-1)
}
// 5-arg usage (hypothetical/legacy): sfw internal-worker diff -old <old> -new <new>
if len(args) >= 5 {
// Assuming indices 2 and 4 based on previous code
oldFile := args[2]
newFile := args[4]
return cli.RunDiffLogic(fsys, oldFile, newFile)
}
return fmt.Errorf("diff worker requires arguments (old <path> new <path>)")
return cli.RunDiffLogic(fsys, os.Stdout, args[1], args[2])

case "scan":
fs := flag.NewFlagSet("scan", flag.ExitOnError)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//go.mod
go 1.24.0

module github.com/BlackVectorOps/semantic_firewall/v3
module github.com/BlackVectorOps/semantic_firewall/v4

require (
github.com/cockroachdb/pebble v1.1.5
Expand Down
39 changes: 27 additions & 12 deletions internal/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,36 @@ import (
"path/filepath"
"strings"

"github.com/BlackVectorOps/semantic_firewall/v3/internal/llm"
"github.com/BlackVectorOps/semantic_firewall/v3/pkg/models"
"github.com/BlackVectorOps/semantic_firewall/v4/internal/llm"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/models"
)

// -- AUDIT COMMAND --

func RunAudit(w io.Writer, oldFile, newFile, commitMsg, apiKey, model, apiBase string) (int, error) {
func RunAudit(w io.Writer, oldFile, newFile, commitMsg, apiKey, model, apiBase string, noSandbox bool) (int, error) {
cleanOld := filepath.Clean(oldFile)
cleanNew := filepath.Clean(newFile)
args := []string{cleanOld, cleanNew}
sb := RealSandboxer{}

var outputBuf bytes.Buffer
// FIX: Pass os.Stderr instead of nil to capture sandbox runtime errors.
err := SandboxExec(sb, &outputBuf, os.Stderr, "diff", args, cleanOld, cleanNew)
if err != nil {
// FAIL-CLOSED: Infrastructure error must not allow bypass.
return 1, fmt.Errorf("audit failed during sandboxed diff: %w", err)

// When the caller opted out of sandboxing -- or when we're already running
// inside one (nested CI containers, runsc-in-runsc) -- skip SandboxExec and
// invoke the diff logic directly. The previous unconditional SandboxExec
// failed closed with "process is already sandboxed; nested sandboxing is
// not supported" and left audit unusable in any pre-sandboxed environment.
if noSandbox || sb.IsSandboxed() {
if err := RunDiffLogic(RealFileSystem{}, &outputBuf, cleanOld, cleanNew); err != nil {
return 1, fmt.Errorf("audit failed during diff: %w", err)
}
} else {
args := []string{cleanOld, cleanNew}
// FIX: Pass os.Stderr instead of nil to capture sandbox runtime errors.
err := SandboxExec(sb, &outputBuf, os.Stderr, "diff", args, cleanOld, cleanNew)
if err != nil {
// FAIL-CLOSED: Infrastructure error must not allow bypass.
return 1, fmt.Errorf("audit failed during sandboxed diff: %w", err)
}
}

var diffOutput models.DiffOutput
Expand Down Expand Up @@ -95,9 +107,12 @@ func RunAudit(w io.Writer, oldFile, newFile, commitMsg, apiKey, model, apiBase s
return 1, fmt.Errorf("json encode failed: %w", err)
}

// FAIL-CLOSED: Strict Verdict Enforcement
switch output.Output.Verdict {
case models.VerdictMatch, models.StatusPreserved:
// FAIL-CLOSED: Strict Verdict Enforcement.
// Normalize case to stay consistent with llm.validateOutput, which accepts
// verdicts case-insensitively; otherwise a valid lowercase "match" would
// fall through to the default branch and be reported as an error.
switch strings.ToUpper(output.Output.Verdict) {
case models.VerdictMatch:
return 0, nil
case models.VerdictLie, models.VerdictSuspicious, models.VerdictError:
return 1, nil
Expand Down
25 changes: 18 additions & 7 deletions internal/cli/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import (
"runtime"
"sync"

"github.com/BlackVectorOps/semantic_firewall/v3/pkg/analysis/ir"
"github.com/BlackVectorOps/semantic_firewall/v3/pkg/analysis/topology"
"github.com/BlackVectorOps/semantic_firewall/v3/pkg/diff"
"github.com/BlackVectorOps/semantic_firewall/v3/pkg/models"
"github.com/BlackVectorOps/semantic_firewall/v3/pkg/storage/jsondb"
"github.com/BlackVectorOps/semantic_firewall/v3/pkg/storage/pebbledb"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/analysis/ir"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/analysis/topology"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/diff"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/models"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/storage/jsondb"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/storage/pebbledb"
"golang.org/x/sync/errgroup"
)

Expand Down Expand Up @@ -148,10 +148,21 @@ func ProcessFilesParallel(fsys FileSystem, files []string, strictMode bool, scan
return ctx.Err()
}

// Robustness: Recover from panics in SSA generation to protect the run
// Robustness: Recover from panics in SSA generation to protect the run.
// Record the panic as a per-file error so it shows up in the JSON output
// and -- critically -- so strict mode still fails closed. Previously a
// panic produced a zero-value FileOutput, left hasErrors false, and let
// --strict silently succeed on a run that crashed mid-analysis.
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "warning: panic recovered analyzing %s: %v\n", f, r)
mu.Lock()
results[idx] = models.FileOutput{
File: f,
ErrorMessage: fmt.Sprintf("panic during analysis: %v", r),
}
hasErrors = true
mu.Unlock()
}
}()

Expand Down
54 changes: 52 additions & 2 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"testing"
"time"

"github.com/BlackVectorOps/semantic_firewall/v3/pkg/analysis/topology"
"github.com/BlackVectorOps/semantic_firewall/v3/pkg/detection"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/analysis/topology"
"github.com/BlackVectorOps/semantic_firewall/v4/pkg/detection"
"golang.org/x/tools/go/packages"
)

Expand Down Expand Up @@ -243,6 +243,56 @@ func TestRunCheckLogic_Isolation(t *testing.T) {
}
}

// panickingFileSystem wraps MockFileSystem but panics on Stat for a specific
// path. Used to exercise the panic-recovery path in ProcessFilesParallel
// without needing to corrupt SSA generation.
type panickingFileSystem struct {
*MockFileSystem
panicOn string
}

func (p *panickingFileSystem) Stat(name string) (os.FileInfo, error) {
if name == p.panicOn {
panic("synthetic panic for test")
}
return p.MockFileSystem.Stat(name)
}

// TestProcessFilesParallel_PanicSurfacedAsError ensures a panic during
// per-file analysis is recorded as an ErrorMessage on the FileOutput and
// flips the hasErrors flag, so --strict fails closed instead of silently
// passing a crashed run.
func TestProcessFilesParallel_PanicSurfacedAsError(t *testing.T) {
mockFS := &panickingFileSystem{
MockFileSystem: &MockFileSystem{
Files: map[string][]byte{
"/app/boom.go": []byte("package main"),
},
},
panicOn: "/app/boom.go",
}

results, hasErrors, err := ProcessFilesParallel(mockFS, []string{"/app/boom.go"}, true, nil)
if err != nil {
t.Fatalf("ProcessFilesParallel returned err: %v", err)
}
if !hasErrors {
t.Fatal("expected hasErrors=true after a recovered panic; got false")
}
if len(results) != 1 {
t.Fatalf("expected 1 result, got %d", len(results))
}
if results[0].ErrorMessage == "" {
t.Error("expected ErrorMessage to be populated for the panicking file")
}
if !strings.Contains(results[0].ErrorMessage, "panic") {
t.Errorf("ErrorMessage should mention panic, got: %q", results[0].ErrorMessage)
}
if results[0].File != "/app/boom.go" {
t.Errorf("File field lost; got %q", results[0].File)
}
}

// TestFileSizeGuard verifies strict size limits are enforced.
func TestFileSizeGuard(t *testing.T) {
mockFS := &MockFileSystem{
Expand Down
Loading
Loading