ONNX
security
malware-detection
Vigil / source /pkg /compactscan /scan.go
turentomer's picture
Make published source self-contained
a2a3348 verified
Raw
History Blame Contribute Delete
8.37 kB
// Package compactscan adapts the existing whole-bundle scanner to the frozen
// compactmodel package contract without invoking any legacy classifier.
package compactscan
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"huggingface.co/turenlabs/Vigil/source/pkg/bundle"
"huggingface.co/turenlabs/Vigil/source/pkg/compactmodel"
)
// Stats contains only aggregate package accounting; no member names or content
// are exposed to machine-readable scoring output.
type Stats struct {
DiscoveredFiles int `json:"discovered_files"`
ModeledFiles int `json:"modeled_files"`
BytesRead int `json:"bytes_read"`
}
type member struct {
rel string
info os.FileInfo
}
// Scan rejects incomplete or ambiguous filesystem trees, invokes bundle.Scan
// to enumerate the whole package, and materializes only members selected by
// the compact preprocessing contract.
func Scan(target string) (compactmodel.Package, Stats, error) {
if target == "" {
return compactmodel.Package{}, Stats{}, errors.New("compact scan target is required")
}
if hasTraversal(target) {
return compactmodel.Package{}, Stats{}, errors.New("compact scan target contains traversal")
}
abs, err := filepath.Abs(target)
if err != nil {
return compactmodel.Package{}, Stats{}, fmt.Errorf("resolve compact scan target: %w", err)
}
targetInfo, err := os.Lstat(abs)
if err != nil {
return compactmodel.Package{}, Stats{}, fmt.Errorf("inspect compact scan target: %w", err)
}
if targetInfo.Mode()&os.ModeSymlink != 0 {
return compactmodel.Package{}, Stats{}, errors.New("compact scan target must not be a symlink")
}
rootPath := abs
if !targetInfo.IsDir() {
if !targetInfo.Mode().IsRegular() || !strings.EqualFold(filepath.Base(abs), "SKILL.md") {
return compactmodel.Package{}, Stats{}, errors.New("compact scan requires a package directory or its SKILL.md")
}
rootPath = filepath.Dir(abs)
}
rootPath, err = filepath.EvalSymlinks(rootPath)
if err != nil {
return compactmodel.Package{}, Stats{}, fmt.Errorf("resolve compact package root: %w", err)
}
if err := preflightPackage(rootPath); err != nil {
return compactmodel.Package{}, Stats{}, fmt.Errorf("compact package preflight failed: %w", err)
}
scanned, err := bundle.Scan(rootPath)
if err != nil {
return compactmodel.Package{}, Stats{}, fmt.Errorf("whole-package scanner failed: %w", err)
}
if scanned.SkillMdPath == "" {
return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner found no SKILL.md")
}
if scanned.ParseErr != nil {
return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner could not parse SKILL.md")
}
if len(scanned.Notes) != 0 {
return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner reported an incomplete or ambiguous scan")
}
root, err := os.OpenRoot(rootPath)
if err != nil {
return compactmodel.Package{}, Stats{}, fmt.Errorf("open compact package root: %w", err)
}
defer root.Close()
members := make([]member, 0, len(scanned.Files)+1)
skillRel, err := filepath.Rel(rootPath, scanned.SkillMdPath)
if err != nil {
return compactmodel.Package{}, Stats{}, errors.New("SKILL.md is outside the package root")
}
members = append(members, member{rel: filepath.ToSlash(skillRel)})
for _, file := range scanned.Files {
if file == nil {
return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner returned an empty member")
}
if file.IsSymlink {
return compactmodel.Package{}, Stats{}, errors.New("compact package contains a symlink")
}
members = append(members, member{rel: filepath.ToSlash(file.RelPath)})
}
lightweight := make([]compactmodel.File, len(members))
seen := make(map[string]struct{}, len(members))
for index := range members {
rel := members[index].rel
if rel == "" || rel == "." || strings.Contains(rel, "\\") || hasTraversal(rel) || filepath.IsAbs(rel) {
return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner returned a non-canonical member")
}
if _, exists := seen[rel]; exists {
return compactmodel.Package{}, Stats{}, errors.New("whole-package scanner returned a duplicate member")
}
seen[rel] = struct{}{}
info, err := root.Lstat(filepath.FromSlash(rel))
if err != nil {
return compactmodel.Package{}, Stats{}, errors.New("package member changed after whole-package scan")
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return compactmodel.Package{}, Stats{}, errors.New("compact package member is not an unlinked regular file")
}
members[index].info = info
lightweight[index] = compactmodel.File{Path: rel, Executable: info.Mode().Perm()&0o111 != 0}
}
// Check every discovered member for hard-link ambiguity, including members
// beyond the preprocessing cap.
for _, item := range members {
file, err := root.Open(filepath.FromSlash(item.rel))
if err != nil {
return compactmodel.Package{}, Stats{}, errors.New("open package member after whole-package scan")
}
opened, statErr := file.Stat()
if statErr != nil {
file.Close()
return compactmodel.Package{}, Stats{}, errors.New("stat package member after whole-package scan")
}
links, linkErr := openedHardLinkCount(file, opened)
closeErr := file.Close()
if linkErr != nil || closeErr != nil || links != 1 || !os.SameFile(item.info, opened) {
return compactmodel.Package{}, Stats{}, errors.New("compact package contains a changed or hard-linked member")
}
}
selected, err := compactmodel.SelectFiles(lightweight)
if err != nil {
return compactmodel.Package{}, Stats{}, err
}
byPath := make(map[string]member, len(members))
for _, item := range members {
byPath[item.rel] = item
}
result := compactmodel.Package{Files: make([]compactmodel.File, 0, len(selected))}
for _, selectedFile := range selected {
item := byPath[selectedFile.Path]
file, err := root.Open(filepath.FromSlash(item.rel))
if err != nil {
return compactmodel.Package{}, Stats{}, errors.New("open selected compact package member")
}
opened, err := file.Stat()
if err != nil || !opened.Mode().IsRegular() || !os.SameFile(item.info, opened) {
file.Close()
return compactmodel.Package{}, Stats{}, errors.New("selected package member changed while opening")
}
links, err := openedHardLinkCount(file, opened)
if err != nil || links != 1 {
file.Close()
return compactmodel.Package{}, Stats{}, errors.New("selected package member has ambiguous links")
}
content, err := sampleFileContent(file, opened.Size())
if err != nil {
file.Close()
return compactmodel.Package{}, Stats{}, err
}
final, statErr := file.Stat()
if statErr != nil {
file.Close()
return compactmodel.Package{}, Stats{}, errors.New("stat selected package member after reading")
}
links, linkErr := openedHardLinkCount(file, final)
closeErr := file.Close()
if linkErr != nil || closeErr != nil || links != 1 || final.Size() != opened.Size() || !os.SameFile(opened, final) {
return compactmodel.Package{}, Stats{}, errors.New("selected package member changed while reading")
}
selectedFile.Content = content
selectedFile.SizeBytes = opened.Size()
result.Files = append(result.Files, selectedFile)
}
document, err := compactmodel.Serialize(result)
if err != nil {
return compactmodel.Package{}, Stats{}, err
}
return result, Stats{
DiscoveredFiles: len(members),
ModeledFiles: document.FileCount,
BytesRead: document.BytesRead,
}, nil
}
func sampleFileContent(file *os.File, size int64) ([]byte, error) {
if size < 0 {
return nil, errors.New("negative package member size")
}
if size <= compactmodel.MaxFileBytes {
data := make([]byte, int(size))
if _, err := io.ReadFull(file, data); err != nil {
return nil, errors.New("read complete package member")
}
return data, nil
}
half := compactmodel.MaxFileBytes / 2
data := make([]byte, compactmodel.MaxFileBytes)
if _, err := file.ReadAt(data[:half], 0); err != nil {
return nil, errors.New("read package member head")
}
if _, err := file.ReadAt(data[half:], size-int64(half)); err != nil {
return nil, errors.New("read package member tail")
}
return data, nil
}
func hasTraversal(value string) bool {
value = strings.ReplaceAll(value, "\\", "/")
for _, part := range strings.Split(value, "/") {
if part == ".." {
return true
}
}
return false
}