Skip to content

feat(modules): bridge fingerprint modules into framework detection#359

Open
TBX3D wants to merge 8 commits into
vmfunc:mainfrom
TBX3D:feat/dialect-bridge-core
Open

feat(modules): bridge fingerprint modules into framework detection#359
TBX3D wants to merge 8 commits into
vmfunc:mainfrom
TBX3D:feat/dialect-bridge-core

Conversation

@TBX3D

@TBX3D TBX3D commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

a fingerprint module and a framework detector can independently identify the same technology. this registers default-confidence, root-scoped fingerprint modules into the framework detector registry so framework detection covers yaml-defined technologies, and keeps the two engines from naming the same thing twice.

BridgeFingerprints is called from Run before scanAllTargets. it has to sit there: the per-target framework scan runs inside the -concurrency worker pool, so bridging any later would not reach it, and loading up front keeps the pool off the shared registry write path. the module set is now loaded once per run rather than once per target.

BridgeFingerprints returns the ids it bridged, and an implicit module run (-all-modules, -module-tags) skips them, so the technology is reported by the framework engine alone. naming a module with -modules is a direct request and still runs it. the two engines disagree on one boundary, the module engine fires at score >= confidence and the framework engine at score > threshold, so a bridged module sitting exactly at 0.5 is reported by neither in an implicit run. that is the residual bridgeableToFramework documents.

the bridged name is the module id and frameworks.Register clobbers by name, so a module called "WordPress" would have replaced the builtin detector. frameworks.RegisterIfAbsent does the check and the insert under one lock and the bridge refuses the name instead.

also marks the legacy custom-signature (signatures dir) path as deprecated in favour of fingerprint modules, without removing it.

stacked on #355 (shared sifpath/httpx helpers) and #356 (fingerprint module type), both open. this branch includes their commits so it builds standalone; rebasing after they merge should drop those as already-applied.

@TBX3D
TBX3D requested a review from vmfunc as a code owner July 9, 2026 22:59
@github-actions github-actions Bot added size/xl 500+ lines changed scan changes to scan engine modules changes to scan modules docs documentation changes tests test changes labels Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

pr summary

14 files changed (+1194 -37)

category files
go source 13
tests 6

@codecov-commenter

codecov-commenter commented Jul 9, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 64.32432% with 66 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@a38ba0a). Learn more about missing BASE report.

Files with missing lines Patch % Lines
sif.go 0.00% 33 Missing ⚠️
internal/modules/bridge.go 75.51% 9 Missing and 3 partials ⚠️
internal/modules/fingerprint.go 86.04% 6 Missing and 6 partials ⚠️
internal/scan/frameworks/detector.go 0.00% 7 Missing ⚠️
internal/modules/yaml.go 77.77% 1 Missing and 1 partial ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #359   +/-   ##
=======================================
  Coverage        ?   65.05%           
=======================================
  Files           ?       90           
  Lines           ?     8029           
  Branches        ?        0           
=======================================
  Hits            ?     5223           
  Misses          ?     2406           
  Partials        ?      400           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@TBX3D
TBX3D force-pushed the feat/dialect-bridge-core branch from e2ba979 to 76ae304 Compare July 9, 2026 23:53

@vmfunc vmfunc left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fingerprint module type itself is solid. scorer parity with MatchSignatures is pinned properly, and the zero-weight remap divergence + strict-threshold-at-0.5 edges are exactly right.

but the bridge doesn't run. BridgeFingerprints has no caller, grep it, only the definition and the tests reference it. at runtime nothing ever lands in the framework registry, so as landed this is inert scaffolding, not what the title says.

and even once you wire it, it doesn't dedup. the fingerprint module still executes in the module engine and reports its own finding (your bridge comment says as much), and registering it as a framework detector just adds a second surface, the framework engine already collapses to one best. so the tech shows up in both the module output and the framework output regardless. bridging never removes the native report. if the plan is to drop bridged fingerprint modules from the module run, that logic isn't in here.

so before this goes in: is the wiring a follow-up, and where does the native finding get suppressed? one of those has to exist for this to do what it claims. and fix the description, it states the dedup as done.

minor: bridged name is def.ID and frameworks.Register clobbers by name, so a module id colliding with a builtin ("WordPress", "Django"...) silently overwrites the builtin detector. skip-or-warn on an existing key.

@TBX3D
TBX3D force-pushed the feat/dialect-bridge-core branch from 76ae304 to b84b14c Compare July 22, 2026 22:18
Copilot AI review requested due to automatic review settings July 22, 2026 22:18
@TBX3D

TBX3D commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

you are right on both counts and the description was the worst of it, sorry, it
described a dedup that nothing performed.

wiring: BridgeFingerprints is now called from Run before scanAllTargets. it has
to be there rather than anywhere later, because the per-target framework scan
runs inside the -concurrency worker pool, so bridging after that point never
reaches the detection it is meant to feed. that also meant loading the module set
once per run instead of once per target, which the pool wanted anyway.

dedup: BridgeFingerprints returns the ids it bridged and an implicit module run
(-all-modules, -module-tags) skips them, so the framework engine reports the
technology alone. naming a module with -modules is a direct request and still
runs it, which felt less surprising than silently dropping something explicitly
asked for. the 0.5 boundary you would expect from that: the module engine fires
at score >= confidence and the framework engine at score > threshold, so a
bridged module sitting exactly at 0.5 is reported by neither in an implicit run.
that is the same residual bridgeableToFramework already documents, and it is
called out in the comment at the call site. happy to close it instead by
suppressing at report time if you would rather not carry it.

collision: added frameworks.RegisterIfAbsent, check and insert under one lock,
and the bridge refuses a taken name rather than replacing it. covered by a test
that registers a detector first and asserts the original still matches its own
marker afterwards.

PR description rewritten to describe what the code does.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Bridges default-confidence root-scoped fingerprint modules into the framework detector registry to avoid double-reporting the same technology, while keeping the legacy signatures/ path working (now with a deprecation notice).

Changes:

  • Add fingerprint YAML module type with weighted scoring, optional version extraction, and result confidence propagation.
  • Bridge eligible fingerprint modules into internal/scan/frameworks detectors and skip bridged modules during implicit module runs to dedupe results.
  • Centralize per-user ~/.config/sif/... (and Windows equivalent) path resolution via internal/sifpath, update loaders, add extensive tests, and document fingerprint modules.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
sif.go Load modules once per run; bridge fingerprint modules into framework detection; skip bridged modules in implicit module execution
internal/sifpath/sifpath.go New helper to resolve per-user sif subdirectories consistently
internal/sifpath/sifpath_test.go Tests for expected per-user directory layout and sibling relationships
internal/scan/frameworks/detector.go Add RegisterIfAbsent to prevent user-supplied name collisions from clobbering built-ins
internal/scan/frameworks/detect.go Use shared httpx.ReadCappedBody for consistent body size limiting
internal/scan/frameworks/custom.go Switch signatures dir resolution to sifpath; emit deprecation notice for legacy custom signatures
internal/scan/frameworks/custom_backcompat_test.go Backcompat test ensuring legacy custom signatures still parse/score
internal/scan/frameworks/custom_deprecation_test.go Test that deprecation notice is logged while still loading legacy signatures
internal/modules/yaml.go Parse/validate/execute new fingerprint module type; expose wrapper definition to in-package bridge
internal/modules/module.go Add TypeFingerprint; add Finding.Confidence for fingerprint findings
internal/modules/loader.go Use sifpath for user modules directory resolution
internal/modules/executor.go Use shared httpx.ReadCappedBody cap for HTTP modules
internal/modules/fingerprint.go New fingerprint module implementation: validation, scoring, version extraction
internal/modules/bridge.go New bridge to register eligible fingerprint modules as framework detectors and report bridged IDs
internal/modules/fingerprint_type_test.go Tests for fingerprint scoring and confidence threshold behavior
internal/modules/fingerprint_parse_test.go Round-trip parse/validate/execute tests + validation/default-confidence behavior
internal/modules/fingerprint_equivalence_test.go Tests pinning scorer equivalence (and known divergence boundaries)
internal/modules/fingerprint_bridge_test.go Tests for bridging rules, collision safety, and framework participation
internal/httpx/httpx.go Add shared MaxBodySize and ReadCappedBody helper
docs/modules.md Document fingerprint module type and YAML schema

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sif.go Outdated
Comment on lines +61 to +75
func (app *App) loadModules() {
if app.modulesLoaded {
return
}
loader, err := modules.NewLoader()
if err != nil {
log.Warnf("Failed to create module loader: %v", err)
return
}
if err := loader.LoadAll(); err != nil {
log.Warnf("Failed to load modules: %v", err)
}
builtin.Register()
app.modulesLoaded = true
}
Comment thread sif.go
Comment on lines +763 to +776
keep := func(ms []modules.Module) []modules.Module {
if len(app.bridgedFingerprints) == 0 {
return ms
}
out := ms[:0]
for _, m := range ms {
if id := m.Info().ID; app.bridgedFingerprints[id] {
log.Debugf("Skipping %s: covered by framework detection", id)
continue
}
out = append(out, m)
}
return out
}
Comment on lines +23 to +36
// UserSubdir returns the per-user sif configuration subdirectory for name (for
// example "modules" or "signatures"). It preserves sif's historical layout:
// ~/.config/sif/<name> on unix-like systems and %LOCALAPPDATA%\sif\<name> on
// windows.
func UserSubdir(name string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
if runtime.GOOS == "windows" {
return filepath.Join(home, "AppData", "Local", "sif", name), nil
}
return filepath.Join(home, ".config", "sif", name), nil
}
Comment on lines 154 to 156
output.Module("FRAMEWORK").Info("Loaded %d custom signatures", len(detectors))
output.Module("FRAMEWORK").Info("~/.config/sif/signatures is deprecated; write type: fingerprint modules under ~/.config/sif/modules instead")
}
Comment on lines +53 to +76
func captureStdout(t *testing.T, fn func()) string {
t.Helper()

r, w, err := os.Pipe()
if err != nil {
t.Fatalf("pipe: %v", err)
}
saved := os.Stdout
os.Stdout = w
output.SetSilent(false)

ch := make(chan string, 1)
go func() {
data, _ := io.ReadAll(r)
ch <- string(data)
}()

fn()

os.Stdout = saved
output.SetSilent(false)
w.Close()
return <-ch
}
@TBX3D

TBX3D commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

follow-up on a regression I introduced in the wiring commit.

folding the two module-load sites into one helper made loadModules swallow a
loader construction failure and warn, but -list-modules used to treat that as
fatal. it would have printed an empty module list and exited 0.

loadModules now returns an error: -list-modules propagates it, a scan still
downgrades to a warning, which is what each path did before they shared the
helper.

also stopped the bridged-module filter from reusing the input slice's backing
array; it builds a new slice now, so it cannot disturb a caller's view of the
registry if All()/ByTag() ever hand back shared storage.

@TBX3D
TBX3D force-pushed the feat/dialect-bridge-core branch from 762d3d0 to dd2c870 Compare July 22, 2026 23:04
@TBX3D

TBX3D commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

rebased onto current main now that #355 has landed. the two shared-helper
commits dropped as already-upstream, as the description predicted, so this is
down to the 8 commits that are actually the fingerprint type plus the bridge.

heads up that CI on this branch (and a few others) is currently red for a reason
unrelated to the change: main does not build at 2ce359a,
internal/modules/executor.go:197 references MaxBodySize after #355 moved it to
internal/httpx. #374 fixes it. verified this branch builds and tests clean with
that one-line fix applied on top.

TBX3D added 8 commits July 22, 2026 16:42
a `fingerprint` module identifies a technology by weighted body/header
signatures scored into a confidence, with an optional version regex, rather
than a boolean match. it fires one finding carrying the score once it reaches
the threshold (default 0.5). this is the framework detectors' scoring in the
module format, so a custom tech fingerprint lives alongside other modules.

validated at load (signatures present, non-empty patterns, finite weights,
confidence in [0,1], version regex compiles) and covered by yaml round-trip,
validation, header, version and default-threshold tests. documented in
docs/modules.md. shares the response body cap via httpx.ReadCappedBody.
…orting

BridgeFingerprints had no caller. nothing ever reached the framework
registry at runtime, so the bridge was scaffolding rather than the feature
the title claims.

call it before the target loop, gated on -framework. it has to sit up
there: the per-target framework scan runs long before the module loader
would otherwise be touched, so bridging any later has no effect on the
detection it is meant to feed.

bridging alone would then name a technology twice, once from the module
engine and once from the framework engine, since the module keeps running
natively. BridgeFingerprints now returns the ids it bridged and an implicit
module run (-all-modules, -module-tags) skips them. naming a module with
-modules is a direct request and still runs it.

the two engines disagree on one boundary, the module engine fires at
score >= confidence and the framework engine at score > threshold, so a
bridged module sitting exactly at 0.5 is reported by neither in an implicit
run. that is the residual bridgeableToFramework already documents.

also refuse to bridge onto a name that is already registered.
frameworks.Register clobbers by name and the bridged name is the module id,
so a module called "WordPress" silently replaced the builtin detector. add
frameworks.RegisterIfAbsent for the check and the insert under one lock.

module loading is now behind app.loadModules, which loads once per run
rather than once per target.
@TBX3D
TBX3D force-pushed the feat/dialect-bridge-core branch from dd2c870 to 7c75c6e Compare July 22, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs documentation changes modules changes to scan modules scan changes to scan engine size/xl 500+ lines changed tests test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants