Skip to content

Commit 76ae304

Browse files
committed
feat(frameworks): deprecate signatures dir in favor of fingerprint modules
1 parent 3b2a65e commit 76ae304

4 files changed

Lines changed: 85 additions & 8 deletions

File tree

internal/modules/bridge.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121

2222
// bridgeableToFramework reports whether def is a fingerprint whose semantics
2323
// the framework engine can reproduce exactly: root path, default confidence,
24-
// all weights > 0 (the C2 shared domain). anything else stays module-only.
24+
// all weights > 0 (the shared domain). anything else stays module-only.
2525
//
2626
// even on this domain the firing boundary still differs at score == 0.5: the
2727
// module engine fires at score >= confidence, the framework engine at
@@ -76,7 +76,7 @@ func bridgeFingerprint(def *YAMLModule) (bool, string) {
7676
// bridgedDetector adapts a bridgeable fingerprint module into a
7777
// frameworks.Detector. structurally the same as frameworks' own (unexported)
7878
// customDetector; kept as a small local copy here rather than exporting that
79-
// type, since frameworks cannot import modules (1.5, avoids an import cycle).
79+
// type, since frameworks cannot import modules and this avoids an import cycle.
8080
type bridgedDetector struct {
8181
frameworks.BaseDetector
8282
versionRe *regexp.Regexp

internal/modules/fingerprint_bridge_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
// okFingerprintDef builds a root-path, default-confidence, all-positive-weight
1212
// fingerprint module: the one shape the framework engine can reproduce exactly
13-
// (C2's shared domain).
13+
// (the shared domain).
1414
func okFingerprintDef(id string) *YAMLModule {
1515
return &YAMLModule{
1616
ID: id,
@@ -80,7 +80,7 @@ func TestBridgeFingerprint_CustomConfidence_Refuses(t *testing.T) {
8080

8181
// TestBridgeFingerprint_MatchesNativeAcrossSampledInputs proves the bridged
8282
// detector and the native module scorer agree on the shared domain, mirroring
83-
// C2's TestScorerEquivalenceSharedDomain but through the bridge itself.
83+
// TestScorerEquivalenceSharedDomain but through the bridge itself.
8484
func TestBridgeFingerprint_MatchesNativeAcrossSampledInputs(t *testing.T) {
8585
def := okFingerprintDef("fp-bridge-sampled")
8686
registered, reason := bridgeFingerprint(def)
@@ -116,12 +116,12 @@ func TestBridgeFingerprint_MatchesNativeAcrossSampledInputs(t *testing.T) {
116116
}
117117

118118
// TestBridgeFingerprint_ExactlyHalfBoundaryDiverges pins the one documented
119-
// residual (3.4): on the shared domain the scores agree, but the module
120-
// engine fires at score >= threshold (inclusive) while the framework engine's
121-
// gate (detectionThreshold, applied by the caller as best.confidence <=
119+
// residual: on the shared domain the scores agree, but the module engine
120+
// fires at score >= threshold (inclusive) while the framework engine's gate
121+
// (detectionThreshold, applied by the caller as best.confidence <=
122122
// detectionThreshold) is exclusive of exactly 0.5. this test only pins the
123123
// score-equality half from inside the bridge; the exclusivity of the
124-
// framework gate itself is proven by TestFrameworkThresholdIsStrict (C3).
124+
// framework gate itself is proven by TestFrameworkThresholdIsStrict.
125125
func TestBridgeFingerprint_ExactlyHalfBoundaryDiverges(t *testing.T) {
126126
def := okFingerprintDef("fp-bridge-half")
127127
def.Fingerprint.Signatures = []FPSignature{

internal/scan/frameworks/custom.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ func loadCustomDetectorsFromDir(dir string) int {
152152
}
153153
if len(detectors) > 0 {
154154
output.Module("FRAMEWORK").Info("Loaded %d custom signatures", len(detectors))
155+
output.Module("FRAMEWORK").Info("~/.config/sif/signatures is deprecated; write type: fingerprint modules under ~/.config/sif/modules instead")
155156
}
156157
return len(detectors)
157158
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package frameworks
2+
3+
import (
4+
"io"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
10+
"github.com/vmfunc/sif/internal/output"
11+
)
12+
13+
// TestLegacyCustomSignatureDeprecationLogged extends the backcompat proof in
14+
// TestLegacyCustomSignatureStillLoads (custom_backcompat_test.go): the legacy
15+
// signatures/ dir must keep loading AND now also emit one deprecation notice
16+
// steering users at the unified fingerprint-module surface. it must not fail,
17+
// stop loading, or migrate anything.
18+
func TestLegacyCustomSignatureDeprecationLogged(t *testing.T) {
19+
dir := t.TempDir()
20+
path := filepath.Join(dir, "acme.yaml")
21+
yamlSrc := "name: acme\n" +
22+
"signatures:\n" +
23+
" - pattern: \"X-Acme\"\n" +
24+
" weight: 1\n" +
25+
" header: true\n"
26+
if err := os.WriteFile(path, []byte(yamlSrc), 0o600); err != nil {
27+
t.Fatal(err)
28+
}
29+
30+
var n int
31+
stdout := captureStdout(t, func() {
32+
n = loadCustomDetectorsFromDir(dir)
33+
})
34+
35+
if n != 1 {
36+
t.Fatalf("loadCustomDetectorsFromDir loaded %d detectors, want 1", n)
37+
}
38+
if _, ok := GetDetector("acme"); !ok {
39+
t.Fatal("acme detector did not register")
40+
}
41+
if !strings.Contains(stdout, "Loaded 1 custom signatures") {
42+
t.Fatalf("missing load-count line, got: %q", stdout)
43+
}
44+
if !strings.Contains(stdout, "deprecated") {
45+
t.Fatalf("missing deprecation notice, got: %q", stdout)
46+
}
47+
}
48+
49+
// captureStdout swaps os.Stdout for a pipe and repoints output's sink at it
50+
// via SetSilent(false), which reads os.Stdout at call time (see
51+
// internal/output/silent_test.go for the same idiom), then runs fn and
52+
// returns everything written.
53+
func captureStdout(t *testing.T, fn func()) string {
54+
t.Helper()
55+
56+
r, w, err := os.Pipe()
57+
if err != nil {
58+
t.Fatalf("pipe: %v", err)
59+
}
60+
saved := os.Stdout
61+
os.Stdout = w
62+
output.SetSilent(false)
63+
64+
ch := make(chan string, 1)
65+
go func() {
66+
data, _ := io.ReadAll(r)
67+
ch <- string(data)
68+
}()
69+
70+
fn()
71+
72+
os.Stdout = saved
73+
output.SetSilent(false)
74+
w.Close()
75+
return <-ch
76+
}

0 commit comments

Comments
 (0)