photoprism/pkg/http/security/cmd/scanpathsgen/main.go
Michael Mayer f2aabb9bcd Security: Add gosec fixes with shared URL and fs validation helpers
Signed-off-by: Michael Mayer <michael@photoprism.app>
2026-03-03 16:38:41 +01:00

154 lines
3.9 KiB
Go

package main
import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"hash/fnv"
"os"
"path/filepath"
"sort"
"strings"
"github.com/photoprism/photoprism/pkg/fs"
)
// scanPathEntry stores the source line and hash for deterministic output ordering.
type scanPathEntry struct {
Hash uint64
Path string
}
// main reads scanner paths from a text file and writes a hashed Go map.
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "scanpathsgen: %v\n", err)
os.Exit(1)
}
}
// run parses command-line arguments and executes generation.
func run() error {
inPath := flag.String("in", "resources/scan-paths.txt", "input file with one path per line")
outPath := flag.String("out", "scan-paths.go", "output Go source file")
flag.Parse()
entries, err := loadEntries(*inPath)
if err != nil {
// Allow public/community contributors to run "make generate"
// without requiring private scanner path resources.
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
source := generateSource(entries)
// Repository output directory should be readable/executable for maintainers.
//nolint:gosec // G301: Non-secret generated source path.
if err := os.MkdirAll(filepath.Dir(*outPath), 0o755); err != nil {
return fmt.Errorf("create output directory: %w", err)
}
// Generated Go source is committed to the repository and intentionally readable.
//nolint:gosec // G306: Non-secret generated source file.
if err := os.WriteFile(*outPath, source, 0o644); err != nil {
return fmt.Errorf("write output file: %w", err)
}
return nil
}
// loadEntries loads, validates, hashes, and sorts scan path entries.
func loadEntries(path string) ([]scanPathEntry, error) {
file, err := openInput(path)
if err != nil {
return nil, fmt.Errorf("open input file %s: %w", path, err)
}
defer file.Close()
dedup := make(map[string]struct{})
byHash := make(map[uint64]string)
entries := make([]scanPathEntry, 0, 256)
scanner := bufio.NewScanner(file)
lineNo := 0
for scanner.Scan() {
lineNo++
line := strings.TrimSpace(scanner.Text())
line = strings.TrimSuffix(line, "\r")
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if !strings.HasPrefix(line, "/") {
return nil, fmt.Errorf("line %d must start with '/': %q", lineNo, line)
}
if _, ok := dedup[line]; ok {
continue
}
dedup[line] = struct{}{}
hash := hashPath(line)
if prev, collision := byHash[hash]; collision && prev != line {
return nil, fmt.Errorf("hash collision detected between %q and %q", prev, line)
}
byHash[hash] = line
entries = append(entries, scanPathEntry{Hash: hash, Path: line})
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("read input file: %w", err)
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].Hash == entries[j].Hash {
return entries[i].Path < entries[j].Path
}
return entries[i].Hash < entries[j].Hash
})
return entries, nil
}
// openInput opens a regular input file from a normalized path.
func openInput(path string) (*os.File, error) {
cleanPath := filepath.Clean(path)
if _, err := fs.StatFile(cleanPath); err != nil {
return nil, err
}
//nolint:gosec // G304: User-provided path is normalized and validated above.
return os.Open(cleanPath)
}
// generateSource renders Go source code for hashed scan path lookup.
func generateSource(entries []scanPathEntry) []byte {
var out bytes.Buffer
out.WriteString("// Code generated by go generate ./pkg/http/security; DO NOT EDIT.\n\n")
out.WriteString("package security\n\n")
out.WriteString("// ScanPathHashes contains lightweight hash keys for scanner probe paths.\n")
out.WriteString("var ScanPathHashes = map[uint64]bool{\n")
for _, entry := range entries {
fmt.Fprintf(&out, "\t0x%016x: true,\n", entry.Hash)
}
out.WriteString("}\n")
return out.Bytes()
}
// hashPath returns the same FNV-1a hash used at runtime for path matching.
func hashPath(path string) uint64 {
h := fnv.New64a()
_, _ = h.Write([]byte(path))
return h.Sum64()
}