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
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ Checks if a file contains a specific string. Useful for tools configured inside
"pyproject.toml" = ["[tool.ruff]"]
```

### yaml_resources (high confidence)

Matches Kubernetes-style YAML resources by API group and, optionally, kind.
Both values must occur in the same YAML document.

```toml
[[detect.yaml_resources]]
api_groups = ["argoproj.io"]
kinds = ["Application", "AppProject", "ApplicationSet"]
```

### key_exists (medium confidence)

Checks if a dot-separated key path exists in a JSON or TOML file. Useful for checking if a tool has configuration in a structured file.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ Language ecosystems and development tools across multiple categories.

**Languages:** Ada, C, C#, C++, COBOL, Clojure, Common Lisp, Crystal, D, Dart, Deno, Elixir, Elm, Emacs Lisp, Erlang, F#, Fortran, GDScript, Gleam, Go, Groovy, Haskell, Haxe, Java, JavaScript, Julia, Kotlin, Lua, Mojo, Nim, Nix, OCaml, Objective-C, Odin, PHP, Perl, Prolog, Python, R, Racket, Roc, Ruby, Rust, Scala, Scheme, Solidity, Swift, Tcl, TypeScript, V, VHDL, Verilog, Zig.

**Package Managers:** Alire, Bun, Bundler, Cabal, Cargo, CocoaPods, Composer, Conan, Conda, DUB, Deno Modules, Flit, Gleam Packages, Go Modules, Gradle, Hatch, Haxelib, Maven, Mix, Nix Flakes, NuGet, PDM, Pipenv, Pkg, Poetry, Quicklisp, Shards, Swift Package Manager, Yarn, cpanm, dotnet CLI, elm, fpm, npm, opam, pip, pnpm, pub, rebar3, renv, sbt, setuptools, uv, vcpkg.
**Package Managers:** Alire, Bun, Bundler, Cabal, Cargo, CocoaPods, Composer, Conan, Conda, DUB, Deno Modules, Flit, Gleam Packages, Go Modules, Gradle, Hatch, Haxelib, Jsonnet Bundler, Maven, Mint, Mix, Nix Flakes, NuGet, PDM, Pipenv, Pkg, Poetry, Quicklisp, Shards, Stack, Swift Package Manager, Yarn, cpanm, dotnet CLI, elm, fpm, npm, opam, pip, pnpm, pub, rebar3, renv, sbt, setuptools, uv, vcpkg.

**Test:** ASV, AVA, Alcotest, Artillery, Bats, BenchmarkTools.jl, Bruno, Catch2, Cucumber, Cypress, EUnit, ExUnit, Gatling, Ginkgo, Google Test, Hspec, Hurl, JUnit, Jasmine, Jest, Kotest, Lighthouse CI, Locust, MSW, Minitest, Mocha, Newman, PHPUnit, Pest, Playwright, REST Client, RSpec, ScalaTest, Selenium, Testify, Testing Library, Vitest, XCTest, axe-core, benchmark-ips, cargo test, clojure.test, criterion, crystal spec, dart test, deno test, dotnet test, gleam test, go test, hyperfine, k6, kotlin.test, nf-test, pytest, pytest-benchmark, tape, testament, testthat, tinytest, tox, vdiffr, zig test.

Expand All @@ -304,7 +304,7 @@ Language ecosystems and development tools across multiple categories.

**Docs:** Docsify, Documenter.jl, Docusaurus, Dokka, Doxygen, ExDoc, Hugo, Javadoc, Jekyll, MkDocs, MultiQC, MyST-Parser, Nextra, Quarto, R Markdown, Read the Docs, Redoc, Sphinx, Starlight, Storybook, Swagger UI, TypeDoc, VitePress, Yard, Zensical, cargo doc, dart doc, deno doc, knitr, mdBook, phpDocumentor, pkgdown, pkgsite, roxygen2.

**Build:** Actix Web, AdonisJS, Angular, Astro, Autotools, Axum, CMake, Django, Dune, Echo, Electron, Eleventy, Ember.js, Express, FastAPI, Fastify, Fiber, Flask, Flutter, Foundry, Gatsby, Gin, GoReleaser, Hardhat, Hono, Invoke, Just, Koa, Laravel, Less, Mage, Make, Meson, NestJS, Next.js, Nextflow, Nuxt, Parcel, Phoenix, PostCSS, Qwik, Rails, Rake, React Native, Remix, Rocket, Rollup, Rspack, SWC, Sass, Sinatra, Snakemake, Spin, Spring Boot, Svelte, SvelteKit, Symfony, Task, Tauri, Vite, Vue, Webpack, cibuildwheel, cross, esbuild, nf-core, targets, tsup.
**Build:** Actix Web, AdonisJS, Angular, Astro, Autotools, Axum, Babashka, BitBake, CMake, Django, Dune, Echo, Electron, Eleventy, Ember.js, Express, FastAPI, Fastify, Fiber, Flask, Flutter, Foundry, Gatsby, Gin, GoReleaser, Hardhat, Hono, Invoke, Just, Koa, Laravel, Less, Mage, Make, Meson, Meteor, NestJS, Next.js, Nextflow, Nuxt, Parcel, Phoenix, PostCSS, Qwik, Rails, Rake, React Native, Remix, Rocket, Rollup, Rspack, SWC, Sass, Sinatra, Snakemake, Spin, Spring Boot, Svelte, SvelteKit, Symfony, Task, Tauri, Vite, Vue, Webpack, cibuildwheel, cross, esbuild, nf-core, targets, tsup.

**Native Ext:** Maturin, Neon, Rustler, meson-python, mkmf, napi-rs, node-gyp, phpize, rb-sys, setuptools Extension, setuptools-rust.

Expand All @@ -320,11 +320,11 @@ Language ecosystems and development tools across multiple categories.

**Container:** Cloud Native Buildpacks, Dev Container, Docker, Docker Compose, Podman.

**Infra:** AWS CDK, Ansible, CloudFormation, DVC, Dockstore, Helm, Kubernetes, Kustomize, Packer, Pulumi, Serverless Framework, Terraform, Vagrant.
**Infra:** AWS CDK, Ansible, Argo CD, Chef, CloudFormation, DVC, Dockstore, Flux, Helm, Helmfile, Kubernetes, Kustomize, OpenTofu, Packer, Pulumi, Puppet, Serverless Framework, Terraform, Vagrant.

**Monorepo:** Bazel, Cargo workspaces, Go workspace, Lerna, Moon, Nx, Pants, Rush, Turborepo, Yarn workspaces, pnpm workspaces.
**Monorepo:** Bazel, Buck, Cargo workspaces, Go workspace, Lerna, Moon, Nx, Pants, Rush, Turborepo, Yarn workspaces, pnpm workspaces.

**Environment:** Flipper, JetBrains IDE, Jupyter, LaunchDarkly, Mise, Pixi, Unleash, VS Code, Volta, asdf, direnv, dotenv, pyenv.
**Environment:** Devbox, Flipper, JetBrains IDE, Jupyter, LaunchDarkly, Mise, Pixi, Unleash, VS Code, Volta, asdf, direnv, dotenv, pyenv.

**i18n:** Crowdin, Fluent, FormatJS, Lingui, Rails i18n, Transifex, gettext, i18next, vue-i18n.

Expand Down
34 changes: 34 additions & 0 deletions cmd/brief/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const scanHelperRootEnv = "BRIEF_SCAN_HELPER_ROOT"
const diffHelperEnv = "BRIEF_DIFF_HELPER"
const submoduleHelperRootEnv = "BRIEF_SUBMODULE_HELPER_ROOT"
const submoduleDiffHelperEnv = "BRIEF_SUBMODULE_DIFF_HELPER"
const yamlResourceDiffHelperEnv = "BRIEF_YAML_RESOURCE_DIFF_HELPER"

func TestScanDefaultsBoundRecursiveDetection(t *testing.T) {
if root := os.Getenv(scanHelperRootEnv); root != "" {
Expand Down Expand Up @@ -79,6 +80,39 @@ func TestDiffAppliesScanOverrides(t *testing.T) {
}
}

func TestDiffReportsYAMLResourceWithUppercaseExtension(t *testing.T) {
if os.Getenv(yamlResourceDiffHelperEnv) != "" {
cmdDiff([]string{"-json", "HEAD"})
os.Exit(0)
}
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not installed")
}

dir := t.TempDir()
initGitScanFixture(t, dir)
writeScanFixture(t, dir, "README.md", "# Example\n")
runGitFixture(t, dir, "add", "README.md")
runGitFixture(t, dir, "commit", "-q", "-m", "initial")
writeScanFixture(t, dir, "manifest.YAML", "apiVersion: argoproj.io/v1alpha1\nkind: Application\n")

cmd := exec.Command(os.Args[0], "-test.run=^TestDiffReportsYAMLResourceWithUppercaseExtension$")
cmd.Dir = dir
cmd.Env = append(os.Environ(), yamlResourceDiffHelperEnv+"=1")
out, err := cmd.Output()
if err != nil {
t.Fatalf("diff command failed: %v", err)
}

var report brief.Report
if err := json.Unmarshal(out, &report); err != nil {
t.Fatalf("parsing diff output: %v\n%s", err, out)
}
if !reportHasTool(&report, "infrastructure", "Argo CD") {
t.Fatalf("tools = %+v, want Argo CD from changed manifest.YAML", report.Tools)
}
}

func TestScanIncludeSubmodulesFlag(t *testing.T) {
if root := os.Getenv(submoduleHelperRootEnv); root != "" {
cmdScan([]string{"-json", "-include-submodules", root})
Expand Down
71 changes: 67 additions & 4 deletions detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const (
DefaultScanLimit = 10000
// DefaultLineCountTimeout bounds external line counters.
DefaultLineCountTimeout = 2 * time.Second
// contentGlobReadLimit bounds content inspected from each glob-matched file.
contentGlobReadLimit = 1 << 20

microsPerMS = 1000.0
scanReadBatchSize = 128
Expand Down Expand Up @@ -606,6 +608,13 @@ func (e *Engine) matchTool(tool *kb.ToolDef) brief.Confidence {
}
}

for _, resource := range tool.Detect.YAMLResources {
if e.hasYAMLResource(resource) {
best = brief.ConfidenceHigh
break
}
}

if len(tool.Detect.Dependencies) > 0 || len(tool.Detect.DevDependencies) > 0 {
if e.hasDependency(tool) {
best = brief.ConfidenceHigh
Expand Down Expand Up @@ -868,6 +877,12 @@ func (e *Engine) loadFileExts() {
// that point outside the root to prevent file disclosure attacks.
// It opens the file via O_NOFOLLOW to avoid TOCTOU races between stat and read.
func (e *Engine) safeReadFile(file string) ([]byte, error) {
return e.safeReadFileLimit(file, 0)
}

// safeReadFileLimit applies safeReadFile's path checks and reads at most limit
// bytes. A non-positive limit reads the complete file.
func (e *Engine) safeReadFileLimit(file string, limit int64) ([]byte, error) {
path := filepath.Join(e.Root, file)
info, err := os.Lstat(path)
if err != nil {
Expand All @@ -889,8 +904,14 @@ func (e *Engine) safeReadFile(file string) ([]byte, error) {
if !targetInfo.Mode().IsRegular() {
return nil, fmt.Errorf("path is not a regular file: %s", file)
}
// Safe symlink within root: read the resolved target directly.
return os.ReadFile(target)
// Safe symlink within root: open the resolved target without following
// a symlink swapped into place after the checks above.
f, err := openNoFollow(target)
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
return readFileLimit(f, limit)
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("path is not a regular file: %s", file)
Expand All @@ -902,7 +923,14 @@ func (e *Engine) safeReadFile(file string) ([]byte, error) {
return nil, err
}
defer func() { _ = f.Close() }()
return io.ReadAll(f)
return readFileLimit(f, limit)
}

func readFileLimit(r io.Reader, limit int64) ([]byte, error) {
if limit > 0 {
r = io.LimitReader(r, limit)
}
return io.ReadAll(r)
}

// contains checks if an exact file or any regular file matching a glob contains
Expand Down Expand Up @@ -936,7 +964,7 @@ func (e *Engine) globContains(pattern string, contentPatterns []string) bool {
if !e.matchesProjectPattern(pattern, rel) {
continue
}
data, err := e.safeReadFile(rel)
data, err := e.safeReadFileLimit(rel, contentGlobReadLimit)
if err == nil && containsAny(string(data), contentPatterns) {
return true
}
Expand All @@ -953,6 +981,41 @@ func containsAny(content string, patterns []string) bool {
return false
}

type yamlResource struct {
APIVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
}

func (e *Engine) hasYAMLResource(signal kb.YAMLResourceInfo) bool {
e.loadProjectFiles()
for _, rel := range e.projectFiles {
ext := strings.ToLower(filepath.Ext(rel))
if ext != ".yaml" && ext != ".yml" {
continue
}

data, err := e.safeReadFileLimit(rel, contentGlobReadLimit)
if err != nil {
continue
}
decoder := yaml.NewDecoder(bytes.NewReader(data))
for {
var resource yamlResource
if err := decoder.Decode(&resource); err != nil {
break
}
group, _, found := strings.Cut(resource.APIVersion, "/")
if !found || !slices.Contains(signal.APIGroups, group) {
continue
}
if len(signal.Kinds) == 0 || slices.Contains(signal.Kinds, resource.Kind) {
return true
}
}
}
return false
}

// matchPathPattern matches slash-separated paths and treats ** as zero or more
// complete path segments.
func matchPathPattern(pattern, name string) bool {
Expand Down
9 changes: 9 additions & 0 deletions detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2695,6 +2695,15 @@ func TestFileContainsGlob(t *testing.T) {
t.Error("exact file_contains path should retain its existing behavior")
}
})

t.Run("bounded read", func(t *testing.T) {
dir := t.TempDir()
writeProjectFile(t, dir, "deploy/large.yaml", strings.Repeat("x", contentGlobReadLimit)+marker)

if New(loadKB(t), dir).contains("**/*.yaml", []string{marker}) {
t.Error("file_contains glob should not inspect content beyond its read limit")
}
})
}

func TestDirectoryGlobPattern(t *testing.T) {
Expand Down
Loading