Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: goreleaser

on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:

jobs:
goreleaser:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
submodules: recursive

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOPATH: "/home/runner/go"
GONOSUMCHECK: "github.com/chainreactors/*"
GONOSUMDB: "github.com/chainreactors/*"
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: test

on:
push:
branches: [master, main]
pull_request:
branches: [master, main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"

- name: Generate
run: go generate ./...
env:
GONOSUMCHECK: "github.com/chainreactors/*"
GONOSUMDB: "github.com/chainreactors/*"

- name: Build
run: go build -v ./...
env:
GONOSUMCHECK: "github.com/chainreactors/*"
GONOSUMDB: "github.com/chainreactors/*"

- name: Test
run: go test -v -count=1 ./...
env:
GONOSUMCHECK: "github.com/chainreactors/*"
GONOSUMDB: "github.com/chainreactors/*"
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ gen
/release
/bbin
/script
/v2/pkg/templates.go
/v2/pkg/templates.go
protocols/file/bench_spray_vs_proton_test.go

# Generated by go generate
pkg/templates.go
pkg/data/
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "templates"]
path = templates
url = https://github.com/chainreactors/templates.git
52 changes: 52 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
project_name: found

before:
hooks:
- go mod tidy
- go generate ./...

upx:
-
enabled: true

builds:
-
main: .
binary: found
goos:
- windows
- linux
- darwin
goarch:
- amd64
- arm64
ignore:
- goos: windows
goarch: arm64
ldflags: "-s -w"
flags:
- -trimpath
env:
- CGO_ENABLED=0

archives:
-
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
format: binary

checksum:
name_template: "{{ .ProjectName }}_checksums.txt"

changelog:
sort: desc
filters:
exclude:
- '^MERGE'
- "{{ .Tag }}"
- "^docs"

release:
github:
owner: chainreactors
name: found
draft: false
114 changes: 114 additions & 0 deletions cmd/collect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cmd

import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/chainreactors/logs"
)

type collectOpts struct {
ZipPath string
BaseDir string
KeepTree bool
Findings []Finding
}

func collectFiles(opts collectOpts) error {
pathSet := make(map[string]bool)
for _, f := range opts.Findings {
pathSet[resolveFilePath(f.FilePath)] = true
}
var paths []string
for p := range pathSet {
paths = append(paths, p)
}

nameMap := buildNameMap(paths, opts.BaseDir, opts.KeepTree)
indexData, _ := json.MarshalIndent(opts.Findings, "", " ")

f, err := os.Create(opts.ZipPath)
if err != nil {
return err
}
defer f.Close()

zw := zip.NewWriter(f)
defer zw.Close()

w, _ := zw.Create("findings.json")
w.Write(indexData)

count := 0
for _, path := range paths {
zipName := nameMap[path]
if err := addFileToZip(zw, path, zipName); err != nil {
logs.Log.Warnf("collect: skip %s: %v", zipName, err)
continue
}
count++
}

logs.Log.Infof("Collected %d files to %s", count, opts.ZipPath)
return nil
}

func addFileToZip(zw *zip.Writer, filePath, zipName string) error {
src, err := os.Open(filePath)
if err != nil {
return err
}
defer src.Close()
w, err := zw.Create(zipName)
if err != nil {
return err
}
_, err = io.Copy(w, src)
return err
}

// resolveFilePath extracts the real filesystem path from a finding path.
// Archive internal paths like "/path/to/backup.tar:secrets.txt" resolve to "/path/to/backup.tar".
func resolveFilePath(path string) string {
if idx := strings.Index(path, ":"); idx > 1 {
return path[:idx]
}
return path
}

// buildNameMap creates the mapping from filesystem path to zip entry name.
// With keepTree=false (default), files are flattened to basename with dedup suffix.
// With keepTree=true, relative directory structure is preserved.
func buildNameMap(paths []string, baseDir string, keepTree bool) map[string]string {
nameMap := make(map[string]string, len(paths))
if keepTree {
for _, p := range paths {
if rel, err := filepath.Rel(baseDir, p); err == nil {
nameMap[p] = filepath.ToSlash(rel)
} else {
nameMap[p] = filepath.Base(p)
}
}
return nameMap
}

used := make(map[string]int)
for _, p := range paths {
base := filepath.Base(p)
if n, ok := used[base]; ok {
ext := filepath.Ext(base)
stem := strings.TrimSuffix(base, ext)
base = fmt.Sprintf("%s_%d%s", stem, n+1, ext)
used[filepath.Base(p)] = n + 1
} else {
used[base] = 0
}
nameMap[p] = base
}
return nameMap
}
129 changes: 129 additions & 0 deletions cmd/ignore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cmd

import (
"os"
"path/filepath"

"github.com/chainreactors/proton/pkg"
"github.com/chainreactors/logs"
"github.com/gobwas/glob"
"gopkg.in/yaml.v3"
)

// ignoreConfig holds post-scan suppression rules for reviewed findings.
//
// This is distinct from pre-scan exclusions (--exclude-id, --etags, extension/directory filters)
// which prevent files or templates from being scanned at all.
// Ignore rules apply AFTER a finding is produced — they suppress known false positives
// or accepted risks that have been reviewed by the user.
type ignoreConfig struct {
Rules []ignoreRule `yaml:"rules"`
}

type ignoreRule struct {
ID string `yaml:"id"`
Paths []string `yaml:"paths"`
Reason string `yaml:"reason"`
}

type compiledIgnore struct {
id glob.Glob
paths []glob.Glob
}

type ignoreFilter struct {
rules []compiledIgnore
suppressed int
}

func loadIgnoreFilter(targets []string, extraPaths []string) *ignoreFilter {
var allRules []ignoreRule

for _, target := range targets {
dir := target
if info, err := os.Stat(target); err == nil && !info.IsDir() {
dir = filepath.Dir(target)
}
if rules := loadIgnoreFile(filepath.Join(dir, ".foundignore.yaml")); rules != nil {
allRules = append(allRules, rules...)
}
}

for _, p := range extraPaths {
if rules := loadIgnoreFile(p); rules != nil {
allRules = append(allRules, rules...)
} else {
logs.Log.Warnf("cannot load ignore file: %s", p)
}
}

if data := pkg.LoadConfig("found_ignore"); len(data) > 0 {
var cfg ignoreConfig
if yaml.Unmarshal(data, &cfg) == nil {
allRules = append(allRules, cfg.Rules...)
}
}

if len(allRules) == 0 {
return nil
}

var compiled []compiledIgnore
for _, r := range allRules {
cr := compiledIgnore{}
if r.ID != "" {
if g, err := glob.Compile(r.ID); err == nil {
cr.id = g
}
}
for _, p := range r.Paths {
if g, err := glob.Compile(p); err == nil {
cr.paths = append(cr.paths, g)
}
}
if cr.id != nil || len(cr.paths) > 0 {
compiled = append(compiled, cr)
}
}

if len(compiled) == 0 {
return nil
}
return &ignoreFilter{rules: compiled}
}

func loadIgnoreFile(path string) []ignoreRule {
data, err := os.ReadFile(path)
if err != nil {
return nil
}
var cfg ignoreConfig
if yaml.Unmarshal(data, &cfg) != nil {
return nil
}
return cfg.Rules
}

// shouldIgnore checks whether a produced finding should be suppressed.
// This runs after the scan — the file was already scanned and the finding was already matched.
func (f *ignoreFilter) shouldIgnore(templateID, relPath string) bool {
if f == nil {
return false
}
for _, rule := range f.rules {
if rule.id != nil && !rule.id.Match(templateID) {
continue
}
if len(rule.paths) == 0 {
f.suppressed++
return true
}
for _, g := range rule.paths {
if g.Match(relPath) {
f.suppressed++
return true
}
}
}
return false
}
Loading
Loading