diff --git a/Makefile b/Makefile index c65f68c..7369d46 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,11 @@ PREFIX ?= /usr/local BINDIR ?= bin MANDIR ?= share/man/man1 +# stamp local builds with the nearest v* tag (or short sha), matching the +# release ci. --match keeps the automated-release-* tags out of the version. +VERSION ?= $(shell git describe --tags --match 'v*' --always --dirty 2>/dev/null | sed 's/^v//') +GO_LDFLAGS = -X main.version=$(VERSION) + define COPYRIGHT_ASCII ╭────────────────────────────────────────────────────────────╮ │ _____________ │ @@ -57,7 +62,7 @@ sif: check_go_version @echo "📁 Current directory: $$(pwd)" @echo "🔧 Go flags: $(GOFLAGS)" @echo "📦 Building package: ./cmd/sif" - $(GO) build -v $(GOFLAGS) ./cmd/sif + $(GO) build -v $(GOFLAGS) -ldflags "$(GO_LDFLAGS)" ./cmd/sif @echo "📊 Build info:" @$(GO) version -m sif @echo "✅ sif built successfully! 🚀" diff --git a/README.md b/README.md index b870f17..3bb3efe 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,20 @@ makepkg -si run `./sif -h` for all options. +## commands + +a couple of subcommands run without scanning: + +```bash +# print the version (release builds are stamped; local builds use git describe) +./sif version + +# show the latest release notes (also -pn) +./sif patchnote +``` + +the first time you run a new release, sif prints that release's notes once. set `SIF_NO_PATCHNOTES=1` to turn that off. + ## modules sif has a modular architecture. modules are defined in yaml and can be extended by users. diff --git a/cmd/sif/main.go b/cmd/sif/main.go index 724faf7..79d176b 100644 --- a/cmd/sif/main.go +++ b/cmd/sif/main.go @@ -20,15 +20,20 @@ import ( "github.com/dropalldatabases/sif" "github.com/dropalldatabases/sif/internal/config" "github.com/dropalldatabases/sif/internal/patchnotes" + ver "github.com/dropalldatabases/sif/internal/version" // Register framework detectors _ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors" ) -// version is filled in at release time via -ldflags "-X main.version=...". +// version is stamped at release time via -ldflags "-X main.version=..."; +// ver.Resolve falls back to the build info or "dev" for other builds. var version = "dev" func main() { + version = ver.Resolve(version) + sif.Version = version + if len(os.Args) > 1 { switch os.Args[1] { case "patchnote", "patchnotes", "-pn", "--patchnotes": diff --git a/docs/usage.md b/docs/usage.md index 1616617..2d788f5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -259,6 +259,28 @@ enable api mode for json output: output is a json object with scan results. +## commands + +these run without scanning a target. + +### version + +print the sif version. release builds are stamped via ldflags, local `make` builds derive it from `git describe`, and `go install`ed builds read it from the module build info: + +```bash +./sif version +``` + +### patchnote + +show the latest release's notes, fetched from github (also `-pn`): + +```bash +./sif patchnote +``` + +the first time you run a new release sif also prints that release's notes once. set `SIF_NO_PATCHNOTES=1` to disable that. + ## examples ### quick recon diff --git a/internal/patchnotes/patchnotes.go b/internal/patchnotes/patchnotes.go index 614acae..7da6495 100644 --- a/internal/patchnotes/patchnotes.go +++ b/internal/patchnotes/patchnotes.go @@ -98,7 +98,9 @@ func Print(tag string) { // then records it so it isn't shown again. best-effort: dev builds, the // SIF_NO_PATCHNOTES opt-out, and any network failure stay silent. func ShowOnce(version string) { - if version == "" || version == "dev" || os.Getenv("SIF_NO_PATCHNOTES") != "" { + // only clean release tags (e.g. 2026.6.7) map to a github release; skip dev + // and pseudo-versions (a commit/dirty build) so we don't make a doomed call. + if version == "" || version == "dev" || strings.ContainsAny(version, "-+") || os.Getenv("SIF_NO_PATCHNOTES") != "" { return } diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..d1250b6 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,67 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +// Package version resolves sif's version from the build. +package version + +import ( + "runtime/debug" + "strings" +) + +// Resolve returns the best version available: the build-time ldflag if it was +// stamped, else the go build info (module tag or vcs revision), else "dev". the +// leading v is dropped so it matches the bare form the rest of sif uses. +func Resolve(ldflag string) string { + if ldflag != "" && ldflag != "dev" { + return normalize(ldflag) + } + if v := fromBuildInfo(); v != "" { + return normalize(v) + } + return "dev" +} + +func fromBuildInfo() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "" + } + if v := info.Main.Version; v != "" && v != "(devel)" { + return v + } + + // no module tag (a local build) - fall back to the commit it was built from + var revision, modified string + for _, s := range info.Settings { + switch s.Key { + case "vcs.revision": + revision = s.Value + case "vcs.modified": + modified = s.Value + } + } + if revision == "" { + return "" + } + if len(revision) > 12 { + revision = revision[:12] + } + if modified == "true" { + revision += "-dirty" + } + return revision +} + +func normalize(v string) string { + return strings.TrimPrefix(v, "v") +} diff --git a/internal/version/version_test.go b/internal/version/version_test.go new file mode 100644 index 0000000..9d2be49 --- /dev/null +++ b/internal/version/version_test.go @@ -0,0 +1,43 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package version + +import "testing" + +func TestResolveLdflag(t *testing.T) { + tests := []struct { + name string + ldflag string + want string + }{ + {"tag with v", "v2026.6.7", "2026.6.7"}, + {"tag without v", "2026.6.7", "2026.6.7"}, + {"pseudo version", "2026.2.17-57-geb33321", "2026.2.17-57-geb33321"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Resolve(tt.ldflag); got != tt.want { + t.Errorf("Resolve(%q) = %q, want %q", tt.ldflag, got, tt.want) + } + }) + } +} + +// with no ldflag, Resolve falls back to build info; in a test binary that's +// non-deterministic, so just assert it never returns an empty string. +func TestResolveFallbackNonEmpty(t *testing.T) { + if Resolve("dev") == "" { + t.Error("Resolve fallback should never be empty") + } +} diff --git a/sif.go b/sif.go index e16a58f..0932571 100644 --- a/sif.go +++ b/sif.go @@ -42,6 +42,9 @@ type App struct { logFiles []string } +// Version is set by main to the resolved build version and shown on the banner. +var Version = "dev" + type UrlResult struct { Url string `json:"url"` Results []ModuleResult @@ -76,7 +79,11 @@ func New(settings *config.Settings) (*App, error) { if !settings.ApiMode { fmt.Println(output.Box.Render(" █▀ █ █▀▀\n ▄█ █ █▀ ")) - fmt.Println(output.Subheading.Render("\nblazing-fast pentesting suite\n\nbsd 3-clause · (c) 2022-2026 vmfunc, xyzeva & contributors\n")) + tagline := "blazing-fast pentesting suite" + if Version != "dev" { + tagline += " · v" + Version + } + fmt.Println(output.Subheading.Render("\n" + tagline + "\n\nbsd 3-clause · (c) 2022-2026 vmfunc, xyzeva & contributors\n")) } else { output.SetAPIMode(true) }