Skip to content

Commit a533a63

Browse files
authored
Merge branch 'main' into optimize-signature-lookup-4077929144382039077
2 parents f0ecc70 + 3a769d9 commit a533a63

15 files changed

Lines changed: 809 additions & 290 deletions

.github/workflows/semantic_analysis.yml

Lines changed: 115 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ jobs:
3636
# Verify binary works
3737
./bin/sfw --version || ./bin/sfw help || true
3838
39+
- name: Vendor Dependencies
40+
# Vendor modules into workspace so they're available inside the sandbox.
41+
# The sandbox only mounts the workspace, not GOMODCACHE (/home/runner/go/pkg/mod).
42+
run: go mod vendor
43+
3944
- name: Determine Mode
4045
id: mode
4146
run: |
@@ -88,129 +93,123 @@ jobs:
8893
echo "has_go_files=false" >> $GITHUB_OUTPUT
8994
fi
9095
91-
- name: Run Semantic Analysis (Sandboxed)
92-
uses: geomys/sandboxed-step@v1.2.0
93-
with:
94-
disable-network: 'true'
95-
# CONTEXT INJECTION:
96-
# We inject vars via interpolation. We write to a local artifact
97-
# instead of trying to hit the runner's step summary directly.
98-
run: |
99-
export MODE="${{ steps.mode.outputs.mode }}"
100-
export WORKTREE_DIR="${{ steps.prep.outputs.worktree_dir }}"
101-
export HAS_GO="${{ steps.prep.outputs.has_go_files }}"
102-
export SFW_SANDBOX_ID="1"
103-
104-
# Strict mode enabled after exports
105-
set -euo pipefail
106-
107-
# Define local report artifact (Safe Write)
108-
REPORT_FILE="scan_report.md"
109-
110-
echo "## Semantic Analysis Report ($MODE)" >> "$REPORT_FILE"
111-
echo "| File | Status | Match % |" >> "$REPORT_FILE"
112-
echo "| :--- | :--- | :--- |" >> "$REPORT_FILE"
113-
114-
if [ "$HAS_GO" != "true" ]; then
115-
echo "No Go files changed." >> "$REPORT_FILE"
116-
exit 0
96+
# Install dependencies for sfw's internal sandbox (AppArmor bypass via aa-exec)
97+
- name: Install Sandbox Dependencies
98+
run: |
99+
sudo apt-get update
100+
sudo apt-get install -y apparmor-utils
101+
102+
- name: Run Semantic Analysis
103+
env:
104+
MODE: ${{ steps.mode.outputs.mode }}
105+
WORKTREE_DIR: ${{ steps.prep.outputs.worktree_dir }}
106+
HAS_GO: ${{ steps.prep.outputs.has_go_files }}
107+
# Use vendored dependencies - sandbox can't access GOMODCACHE
108+
GOFLAGS: "-mod=vendor"
109+
GOPROXY: "off"
110+
run: |
111+
set -euo pipefail
112+
113+
REPORT_FILE="scan_report.md"
114+
115+
echo "## Semantic Analysis Report ($MODE)" >> "$REPORT_FILE"
116+
echo "| File | Status | Match % |" >> "$REPORT_FILE"
117+
echo "| :--- | :--- | :--- |" >> "$REPORT_FILE"
118+
119+
if [ "$HAS_GO" != "true" ]; then
120+
echo "No Go files changed." >> "$REPORT_FILE"
121+
exit 0
122+
fi
123+
124+
ERROR_COUNT=0
125+
LOGIC_FAIL=0
126+
127+
# Process the pre-calculated diff stream
128+
while IFS= read -r -d '' status; do
129+
case "$status" in
130+
R*|C*)
131+
IFS= read -r -d '' old_path
132+
IFS= read -r -d '' new_path
133+
OLD_FILE_REF="$old_path"
134+
NEW_FILE_REF="$new_path"
135+
;;
136+
*)
137+
IFS= read -r -d '' path
138+
OLD_FILE_REF="$path"
139+
NEW_FILE_REF="$path"
140+
;;
141+
esac
142+
143+
if [[ "$NEW_FILE_REF" != *.go ]] && [[ "$OLD_FILE_REF" != *.go ]]; then continue; fi
144+
145+
if [ -f "$NEW_FILE_REF" ]; then
146+
NEW_FILE="$NEW_FILE_REF"
147+
else
148+
NEW_FILE=""
117149
fi
118150
119-
# Check for required tools inside sandbox
120-
if ! command -v jq >/dev/null; then
121-
echo "::error::'jq' is missing from the sandbox environment."
122-
exit 1
151+
OLD_FILE="$WORKTREE_DIR/$OLD_FILE_REF"
152+
if [ ! -f "$OLD_FILE" ]; then
153+
OLD_FILE=""
123154
fi
124155
125-
ERROR_COUNT=0
126-
LOGIC_FAIL=0
127-
128-
# Process the pre-calculated diff stream
129-
while IFS= read -r -d '' status; do
130-
case "$status" in
131-
R*|C*)
132-
IFS= read -r -d '' old_path
133-
IFS= read -r -d '' new_path
134-
OLD_FILE_REF="$old_path"
135-
NEW_FILE_REF="$new_path"
136-
;;
137-
*)
138-
IFS= read -r -d '' path
139-
OLD_FILE_REF="$path"
140-
NEW_FILE_REF="$path"
141-
;;
142-
esac
143-
144-
if [[ "$NEW_FILE_REF" != *.go ]] && [[ "$OLD_FILE_REF" != *.go ]]; then continue; fi
145-
146-
if [ -f "$NEW_FILE_REF" ]; then
147-
NEW_FILE="$NEW_FILE_REF"
148-
else
149-
NEW_FILE=""
150-
fi
151-
152-
OLD_FILE="$WORKTREE_DIR/$OLD_FILE_REF"
153-
if [ ! -f "$OLD_FILE" ]; then
154-
OLD_FILE=""
155-
fi
156-
157-
if [[ -z "$NEW_FILE" ]] && [[ -z "$OLD_FILE" ]]; then continue; fi
158-
159-
if [[ -z "$OLD_FILE" ]]; then
160-
echo "| \`$NEW_FILE_REF\` | New File | N/A |" >> "$REPORT_FILE"
161-
continue
162-
fi
163-
164-
if [[ -z "$NEW_FILE" ]]; then
165-
echo "| \`$OLD_FILE_REF\` | Deleted | N/A |" >> "$REPORT_FILE"
166-
continue
167-
fi
168-
169-
# Execute SFW with stderr capture
170-
if ! OUTPUT=$(./bin/sfw diff "$OLD_FILE" "$NEW_FILE" 2>&1); then
171-
echo "::error::sfw failed to process $NEW_FILE_REF"
172-
ERROR_COUNT=$((ERROR_COUNT + 1))
173-
continue
174-
fi
175-
176-
# Validate JSON
177-
if ! echo "$OUTPUT" | jq -e . >/dev/null 2>&1; then
178-
echo "::error::Invalid JSON output for $NEW_FILE_REF"
179-
ERROR_COUNT=$((ERROR_COUNT + 1))
180-
continue
181-
fi
182-
183-
PCT=$(echo "$OUTPUT" | jq -r '.summary.semantic_match_pct // 0')
184-
MODIFIED=$(echo "$OUTPUT" | jq -r '.summary.modified // 0')
185-
IS_BELOW_100=$(echo "$OUTPUT" | jq -r 'if (.summary.semantic_match_pct // 0) < 100 then "true" else "false" end')
186-
187-
if [ "$IS_BELOW_100" = "true" ]; then
188-
STATUS_ICON="Modified ($MODIFIED)"
189-
echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE"
190-
191-
if [ "$MODE" == "BLOCKER" ]; then
192-
echo "::error file=$NEW_FILE_REF::Logic change detected in safe refactor! ($PCT%)"
193-
LOGIC_FAIL=1
194-
fi
195-
else
196-
STATUS_ICON="Preserved"
197-
echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE"
198-
fi
199-
200-
done < diff_stream.bin
201-
202-
if [ $ERROR_COUNT -gt 0 ]; then
203-
echo "" >> "$REPORT_FILE"
204-
echo "**CI FAILED**: Tool execution failures detected." >> "$REPORT_FILE"
205-
exit 1
156+
if [[ -z "$NEW_FILE" ]] && [[ -z "$OLD_FILE" ]]; then continue; fi
157+
158+
if [[ -z "$OLD_FILE" ]]; then
159+
echo "| \`$NEW_FILE_REF\` | New File | N/A |" >> "$REPORT_FILE"
160+
continue
206161
fi
207162
208-
if [ $LOGIC_FAIL -eq 1 ]; then
209-
echo "" >> "$REPORT_FILE"
210-
echo "**CI FAILED**: Logic changed in 'semantic-safe' PR." >> "$REPORT_FILE"
211-
exit 1
163+
if [[ -z "$NEW_FILE" ]]; then
164+
echo "| \`$OLD_FILE_REF\` | Deleted | N/A |" >> "$REPORT_FILE"
165+
continue
212166
fi
213167
168+
# Execute SFW diff (sfw handles its own sandboxing internally)
169+
if ! OUTPUT=$(./bin/sfw diff "$OLD_FILE" "$NEW_FILE" 2>&1); then
170+
echo "::error::sfw failed to process $NEW_FILE_REF"
171+
ERROR_COUNT=$((ERROR_COUNT + 1))
172+
continue
173+
fi
174+
175+
# Validate JSON
176+
if ! echo "$OUTPUT" | jq -e . >/dev/null 2>&1; then
177+
echo "::error::Invalid JSON output for $NEW_FILE_REF"
178+
ERROR_COUNT=$((ERROR_COUNT + 1))
179+
continue
180+
fi
181+
182+
PCT=$(echo "$OUTPUT" | jq -r '.summary.semantic_match_pct // 0')
183+
MODIFIED=$(echo "$OUTPUT" | jq -r '.summary.modified // 0')
184+
IS_BELOW_100=$(echo "$OUTPUT" | jq -r 'if (.summary.semantic_match_pct // 0) < 100 then "true" else "false" end')
185+
186+
if [ "$IS_BELOW_100" = "true" ]; then
187+
STATUS_ICON="Modified ($MODIFIED)"
188+
echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE"
189+
190+
if [ "$MODE" == "BLOCKER" ]; then
191+
echo "::error file=$NEW_FILE_REF::Logic change detected in safe refactor! ($PCT%)"
192+
LOGIC_FAIL=1
193+
fi
194+
else
195+
STATUS_ICON="Preserved"
196+
echo "| \`$NEW_FILE_REF\` | $STATUS_ICON | **$PCT%** |" >> "$REPORT_FILE"
197+
fi
198+
199+
done < diff_stream.bin
200+
201+
if [ $ERROR_COUNT -gt 0 ]; then
202+
echo "" >> "$REPORT_FILE"
203+
echo "**CI FAILED**: Tool execution failures detected." >> "$REPORT_FILE"
204+
exit 1
205+
fi
206+
207+
if [ $LOGIC_FAIL -eq 1 ]; then
208+
echo "" >> "$REPORT_FILE"
209+
echo "**CI FAILED**: Logic changed in 'semantic-safe' PR." >> "$REPORT_FILE"
210+
exit 1
211+
fi
212+
214213
- name: Publish Analysis Report
215214
if: always()
216215
# This runs on the host, so it has access to GITHUB_STEP_SUMMARY.
@@ -227,4 +226,4 @@ jobs:
227226
run: |
228227
if [ -d "$WORKTREE_DIR" ]; then
229228
git worktree remove --force "$WORKTREE_DIR" 2>/dev/null || rm -rf "$WORKTREE_DIR"
230-
fi
229+
fi

internal/cli/index.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,13 @@ func RunIndexJSON(target string, results []diff.FingerprintResult, name, severit
166166
Created: time.Now().Format("2006-01-02"),
167167
}
168168

169-
scanner.AddSignature(&sig)
170169
indexed = append(indexed, sig)
171170
}
172171

172+
if err := scanner.AddSignatures(indexed); err != nil {
173+
return nil, 0, err
174+
}
175+
173176
if err := scanner.SaveDatabase(dbPath); err != nil {
174177
return nil, 0, err
175178
}

pkg/analysis/ir/benchmark_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package ir_test
2+
3+
import (
4+
"go/token"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
10+
"github.com/BlackVectorOps/semantic_firewall/v3/pkg/analysis/ir"
11+
"golang.org/x/tools/go/packages"
12+
"golang.org/x/tools/go/ssa"
13+
)
14+
15+
func compileForBenchmark(b *testing.B, src, funcName string) *ssa.Function {
16+
b.Helper()
17+
dir, err := os.MkdirTemp("", "ssa-bench-")
18+
if err != nil {
19+
b.Fatalf("failed to create temp dir: %v", err)
20+
}
21+
defer os.RemoveAll(dir)
22+
23+
modPath := filepath.Join(dir, "go.mod")
24+
if err := os.WriteFile(modPath, []byte("module testmod\n\ngo 1.23\n"), 0644); err != nil {
25+
b.Fatalf("failed to create go.mod: %v", err)
26+
}
27+
28+
path := filepath.Join(dir, "main.go")
29+
if err := os.WriteFile(path, []byte(src), 0644); err != nil {
30+
b.Fatalf("write source: %v", err)
31+
}
32+
33+
env := append(os.Environ(), "GO111MODULE=on", "GOPROXY=off", "CGO_ENABLED=0")
34+
35+
cfg := &packages.Config{
36+
Dir: dir,
37+
Mode: packages.LoadAllSyntax,
38+
Fset: token.NewFileSet(),
39+
Env: env,
40+
}
41+
42+
pkgs, err := packages.Load(cfg, "file="+path)
43+
if err != nil {
44+
b.Fatalf("packages.Load: %v", err)
45+
}
46+
if packages.PrintErrors(pkgs) > 0 {
47+
b.Fatal("compilation errors in test source")
48+
}
49+
50+
prog, _, err := ir.BuildSSAFromPackages(pkgs)
51+
if err != nil {
52+
b.Fatalf("BuildSSA: %v", err)
53+
}
54+
55+
for _, pkg := range pkgs {
56+
ssaPkg := prog.Package(pkg.Types)
57+
if ssaPkg == nil {
58+
continue
59+
}
60+
for _, member := range ssaPkg.Members {
61+
if fn, ok := member.(*ssa.Function); ok {
62+
if fn.Name() == funcName || strings.HasSuffix(fn.Name(), "."+funcName) {
63+
return fn
64+
}
65+
}
66+
}
67+
}
68+
69+
b.Fatalf("function %q not found in SSA program", funcName)
70+
return nil
71+
}
72+
73+
func BenchmarkCanonicalizeFunction(b *testing.B) {
74+
// Source code with various instruction types to exercise different paths
75+
src := `package main
76+
77+
func everything(ch chan int, m map[string]interface{}) interface{} {
78+
// Defer
79+
defer func() { recover() }()
80+
81+
res := 0
82+
83+
// Select
84+
select {
85+
case x := <-ch:
86+
// Map Update & Interface
87+
m["val"] = x
88+
res = x
89+
default:
90+
// MakeSlice & Go
91+
go func() { _ = make([]int, 10, 20) }()
92+
res = 1
93+
}
94+
95+
// Type Assert
96+
if val, ok := m["val"].(int); ok {
97+
return val * 2
98+
}
99+
100+
return res
101+
}`
102+
103+
fn := compileForBenchmark(b, src, "everything")
104+
// Use default policy
105+
policy := ir.DefaultLiteralPolicy
106+
107+
b.ResetTimer()
108+
for i := 0; i < b.N; i++ {
109+
// Acquire/Release per iteration to simulate real usage
110+
c := ir.AcquireCanonicalizer(policy)
111+
c.CanonicalizeFunction(fn)
112+
ir.ReleaseCanonicalizer(c)
113+
}
114+
}

0 commit comments

Comments
 (0)