From 96ca98f58c30cdc02b95b75379c4a19365c88e5d Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sun, 23 Aug 2026 23:35:55 +0200 Subject: [PATCH 1/7] refactor(cli): Rework the help text and end-of-run output --- cli/cmd/compile.go | 34 +++++---- cli/cmd/dry_run.go | 34 ++++++++- cli/cmd/exit_codes.go | 36 +++++++++ cli/cmd/health.go | 20 +++-- cli/cmd/project.go | 41 +++++----- cli/cmd/prune.go | 52 +++++++------ cli/cmd/pull.go | 31 ++++---- cli/cmd/rerun_test.go | 115 ++++++++++++++++++++++++++++ cli/cmd/root.go | 8 +- cli/cmd/scan.go | 121 ++++++++++++++++++++++-------- cli/cmd/suggest.go | 87 +++++++++++++++++++-- cli/cmd/summary.go | 47 +++++++++--- cli/cmd/test.go | 27 +++---- cli/cmd/test_approximation_run.go | 17 ++++- cli/cmd/test_init.go | 61 +++++++++------ cli/cmd/test_rule_reachability.go | 23 +++++- cli/cmd/test_rule_run.go | 63 ++++++++++++---- cli/cmd/update.go | 28 ++++--- cli/internal/analyzer/exit.go | 4 +- docs/README.md | 4 +- docs/installation.md | 2 +- docs/usage.md | 40 +++++----- 22 files changed, 671 insertions(+), 224 deletions(-) create mode 100644 cli/cmd/exit_codes.go create mode 100644 cli/cmd/rerun_test.go diff --git a/cli/cmd/compile.go b/cli/cmd/compile.go index 3d33c8d33..965b3973f 100644 --- a/cli/cmd/compile.go +++ b/cli/cmd/compile.go @@ -32,21 +32,28 @@ func currentCompileBuilder(projectPath string) *utils.OpentaintCommandBuilder { // dockerCompileSuggestion builds the "try Docker-based compilation" fallback hint. func dockerCompileSuggestion() output.Suggestion { return output.Suggestion{ - Description: dockerFallbackHintPrefix + "compilation:", + Description: "If the required Java is missing, set JAVA_HOME or compile in a container instead:", Command: utils.BuildCompileCommandWithDocker(currentCompileBuilder(""), ProjectPath, OutputProjectModelPath), } } // compileCmd represents the compile command var compileCmd = &cobra.Command{ - Use: "compile project", - Short: "Compile your Java or Kotlin project", + Use: "compile ", + Short: "Compile a project into a reusable project model", Args: cobra.ExactArgs(1), // require exactly one argument - Long: `This command takes a required path to the project, automatically detects Java/Kotlin build system, modules and dependencies and compiles project model. + Long: `OpenTaint detects the build system, resolves the project's modules and dependencies, and compiles the project into a reusable model. -Arguments: - project - Path to a project to compile (required) -`, +The project argument is the path to the project root and is required. Pass --output to name the project model directory to create; it must not already exist. + +The project model is written to the --output directory and can be reused by later scans without rebuilding. + +Run opentaint pull once before your first compile to fetch the toolchain. Analyze the resulting project model with opentaint scan --project-model.`, + Example: ` # Compile the current directory into a project model + opentaint compile . -o ./model + + # Validate inputs without compiling + opentaint compile . -o ./model --dry-run`, Annotations: map[string]string{"PrintConfig": "true"}, Run: func(cmd *cobra.Command, args []string) { ProjectPath = args[0] @@ -72,25 +79,25 @@ Arguments: sb.Line() } sb.FieldNode("Project", absProjectRoot). - FieldNode("Output project model", absOutputProjectModelPath). + FieldNode("Project model", absOutputProjectModelPath). FieldNode("Autobuilder", utils.ArtifactVersionWithPath(globals.ArtifactByKind("autobuilder"))). Render() if DryRunCompile { out.Blank() failOnInvalidInputs(func() error { return validation.ValidateCompileInputs(absProjectRoot, absOutputProjectModelPath) }) - runDryRun("Compilation") + runDryRun("compilation") return } autobuilderJarPath, err := ensureAutobuilderAvailable() if err != nil { - out.Fatalf("Native compile preparation failed: %s", err) + failf("Native compile preparation failed: %s", err) } compileJavaRunner := newAutobuilderJavaRunner() if _, err := compileJavaRunner.EnsureJava(); err != nil { - out.Fatalf("Failed to resolve Java for compilation: %s", err) + failf("Failed to resolve Java for compilation: %s", err) } if err := out.RunWithSpinner("Compiling project model", func() error { @@ -98,7 +105,8 @@ Arguments: }); err == nil { out.Blank() printCompileSummary(absOutputProjectModelPath) - suggest("To scan project run", utils.BuildScanCommandFromCompile(projectRoot, absOutputProjectModelPath)) + out.Successf("Compilation completed.") + suggest("To scan the compiled project model, run:", utils.BuildScanCommandFromCompile(projectRoot, absOutputProjectModelPath)) } else { out.InteractiveBlank() failWith(1, fmt.Sprintf("Native compile has failed: %s", err), dockerCompileSuggestion()) @@ -109,7 +117,7 @@ Arguments: func init() { rootCmd.AddCommand(compileCmd) - compileCmd.Flags().StringVarP(&OutputProjectModelPath, "output", "o", "", `Path to the result project model`) + compileCmd.Flags().StringVarP(&OutputProjectModelPath, "output", "o", "", `Path to the project model directory to create (required, must not exist)`) _ = compileCmd.MarkFlagRequired("output") compileCmd.Flags().BoolVar(&DryRunCompile, "dry-run", false, "Validate inputs and show what would run without compiling") compileCmd.Flags().StringVar(&CompileLogFile, "log-file", "", "Path to the log file (default: /logs/.log)") diff --git a/cli/cmd/dry_run.go b/cli/cmd/dry_run.go index e0c2f7276..c6086cb4e 100644 --- a/cli/cmd/dry_run.go +++ b/cli/cmd/dry_run.go @@ -1,6 +1,9 @@ package cmd -import "fmt" +import ( + "os" + "strings" +) func failOnInvalidInputs(validate func() error) { if err := validate(); err != nil { @@ -8,6 +11,33 @@ func failOnInvalidInputs(validate func() error) { } } +// runDryRun prints the standard dry-run tail: a status line naming the skipped +// action, then a suggestion to repeat the same invocation without --dry-run. func runDryRun(skippedAction string) { - out.Print(fmt.Sprintf("Dry run mode. Inputs validated. %s skipped.", skippedAction)) + out.Printf("Dry run complete. Inputs validated; %s skipped.", skippedAction) + suggest("To run for real, run:", rerunWithoutDryRun()) +} + +// rerunWithoutDryRun reconstructs the current invocation with the --dry-run +// flag removed, so the dry-run tail can suggest the real run verbatim. It works +// from os.Args, which keeps it correct for every command that shares this tail +// (scan, compile, project, test rule reachability). +func rerunWithoutDryRun() string { + args := []string{"opentaint"} + for _, arg := range os.Args[1:] { + if arg == "--dry-run" || strings.HasPrefix(arg, "--dry-run=") { + continue + } + args = append(args, shellQuote(arg)) + } + return strings.Join(args, " ") +} + +// shellQuote single-quotes an argument that would break when copy-pasted into +// a shell (spaces, quotes); plain arguments pass through unchanged. +func shellQuote(arg string) string { + if arg != "" && !strings.ContainsAny(arg, " \t'\"") { + return arg + } + return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'" } diff --git a/cli/cmd/exit_codes.go b/cli/cmd/exit_codes.go new file mode 100644 index 000000000..5de9a0912 --- /dev/null +++ b/cli/cmd/exit_codes.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "fmt" + + "github.com/seqra/opentaint/internal/analyzer" +) + +// analyzerExitCodeRows renders the forwarded analyzer exit codes (252-255) as +// help rows. The row text is generated from analyzer.ExitMessage so the +// documented codes can never drift from the runtime failure messages. +func analyzerExitCodeRows() string { + rows := "" + for _, code := range []int{analyzer.ExitException, analyzer.ExitOOM, analyzer.ExitTimeout, analyzer.ExitConfigError} { + rows += fmt.Sprintf("\n %-3d %s", code, analyzer.ExitMessage(code)) + } + return rows +} + +// scanExitCodesHelp renders the exit-codes block for commands that forward +// analyzer exit codes but have no test-failure code (scan, test rule +// reachability). +func scanExitCodesHelp(completedLine string) string { + return `Exit codes: + 0 ` + completedLine + ` + 1 General failure (configuration or infrastructure error)` + analyzerExitCodeRows() +} + +// testExitCodesHelp renders the exit-codes block for the test-run commands, +// which add exit code 2 for sample failures. +func testExitCodesHelp(passedLine string) string { + return `Exit codes: + 0 ` + passedLine + ` + 1 General failure (configuration or infrastructure error) + 2 One or more tests failed (false negatives, false positives, or skipped samples)` + analyzerExitCodeRows() +} diff --git a/cli/cmd/health.go b/cli/cmd/health.go index 0954bbbf4..4e3de53bd 100644 --- a/cli/cmd/health.go +++ b/cli/cmd/health.go @@ -27,15 +27,20 @@ type healthComponent struct { var healthCmd = &cobra.Command{ Use: "health", - Short: "Show resolved dependency paths", - Long: `Show the on-disk paths OpenTaint uses for the autobuilder, analyzer, -built-in rules, and Java runtime. + Short: "Show dependency paths and report missing components", + Long: `Show the on-disk paths for the autobuilder, analyzer, built-in rules, and Java runtime, and report whether each component is present. -Use --autobuilder, --analyzer, --rules, or --runtime to select components. When -exactly one component is selected, only its path is printed. The command does -not download artifacts except built-in rules, which are fetched on demand. +Select components with --autobuilder, --analyzer, --rules, or --runtime; with no flag, all four are reported. When exactly one component is selected, only its path is printed, which suits scripting. Only the built-in rules are fetched on demand; no other artifact is downloaded. -The exit code is non-zero when any selected component is missing.`, +The command exits non-zero when any selected component is missing. Download the missing components with opentaint pull.`, + Example: ` # Report all components and their paths + opentaint health + + # Print only the analyzer JAR path, for scripting + opentaint health --analyzer + + # Check the Java runtime + opentaint health --runtime`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runHealth() @@ -105,6 +110,7 @@ func runHealth() error { } sb.Render() if len(missing) > 0 { + out.Suggest("To download the missing components, run:", "opentaint pull") return fmt.Errorf("missing components: %s", strings.Join(missing, ", ")) } return nil diff --git a/cli/cmd/project.go b/cli/cmd/project.go index 60e31d5d2..0e8ff5cd2 100644 --- a/cli/cmd/project.go +++ b/cli/cmd/project.go @@ -181,7 +181,8 @@ func (c *JavaAutobuilderConfig) printProjectSummary(config *project.Config) erro projectYamlPath := filepath.Join(c.outputDir, "project.yaml") c.logProjectSummary(projectYamlPath, config) - suggest("To scan project run", utils.BuildScanCommandFromCompile(c.outputDir, c.outputDir)) + out.Successf("Project model generated.") + suggest("To scan the generated model, run:", utils.BuildScanCommandFromCompile(c.outputDir, c.outputDir)) return nil } @@ -218,16 +219,22 @@ var ( var projectCmd = &cobra.Command{ Use: "project", - Short: "Create a project model directory containing a project.yaml configuration from precompiled JARs or classes", - Long: `Create a project model directory containing a project.yaml configuration from precompiled JARs or classes. + Short: "Create a project model from precompiled JARs or classes", + Long: `Create a project model directly from precompiled JARs or classes, without running a build. OpenTaint inspects the supplied classpath, detects the modules and dependencies, and writes a project.yaml describing the project for later analysis. Use this when you already have compiled artifacts; opentaint compile builds a model from sources instead. -This command generates a project model, automatically detecting dependencies and project structure. -Additional packages have to be specified to enhance the generated configuration. +All inputs are supplied as flags. Provide --source-root for the project sources, one or more --classpath entries for the compiled classes or JARs, and one or more --package names to include. Add --dependency for extra JAR files on the compile classpath. -Examples: - # Classpath analysis - opentaint project --output ./project-model --source-root /path/to/source \ - --classpath /path/to/app.jar --package com.example`, +The project model is written to the required --output directory, which must not already exist, and contains the generated project.yaml. + +Run opentaint pull once beforehand to fetch the autobuilder. Scan the generated model with opentaint scan --project-model .`, + Example: ` # Generate a project model from a compiled JAR + opentaint project --source-root ./src --classpath ./app.jar --package com.example -o ./model + + # Add extra dependency JARs to the classpath + opentaint project --source-root ./src --classpath ./app.jar --dependency ./lib.jar --package com.example -o ./model + + # Validate the inputs without generating anything + opentaint project --source-root ./src --classpath ./app.jar --package com.example -o ./model --dry-run`, Run: func(cmd *cobra.Command, args []string) { config := NewJavaAutobuilder(). WithOutputDir(OutputDir). @@ -269,12 +276,12 @@ Examples: if DryRunProject { failOnInvalidInputs(config.validate) - runDryRun("Project generation") + runDryRun("project-model generation") return } if err := config.Execute(); err != nil { - out.Fatalf("Failed to generate project configuration: %s", err) + failf("Failed to generate project configuration: %s", err) } }, } @@ -282,15 +289,15 @@ Examples: func init() { rootCmd.AddCommand(projectCmd) - projectCmd.Flags().StringVarP(&OutputDir, "output", "o", "", "Output directory for project.yaml") + projectCmd.Flags().StringVarP(&OutputDir, "output", "o", "", "Directory to write the generated project model (required, must not exist)") _ = projectCmd.MarkFlagRequired("output") - projectCmd.Flags().StringVar(&SourceRoot, "source-root", "", "Source root directory") + projectCmd.Flags().StringVar(&SourceRoot, "source-root", "", "Path to the project source root") _ = projectCmd.MarkFlagRequired("source-root") - projectCmd.Flags().StringArrayVar(&Dependencies, "dependency", []string{}, "Project dependencies (JAR files)") - projectCmd.Flags().StringArrayVar(&Packages, "package", []string{}, "Project packages") + projectCmd.Flags().StringArrayVar(&Dependencies, "dependency", []string{}, "Additional dependency JAR file on the compile classpath (repeatable)") + projectCmd.Flags().StringArrayVar(&Packages, "package", []string{}, "Package to include in the generated model (repeatable)") _ = projectCmd.MarkFlagRequired("package") - projectCmd.Flags().StringArrayVar(&Classpaths, "classpath", []string{}, "Classpath entries (classes or JAR files)") + projectCmd.Flags().StringArrayVar(&Classpaths, "classpath", []string{}, "Classpath entry: a compiled classes directory or a JAR file (repeatable)") _ = projectCmd.MarkFlagRequired("classpath") - projectCmd.Flags().BoolVar(&DryRunProject, "dry-run", false, "Validate inputs and show what would run without generating project model") + projectCmd.Flags().BoolVar(&DryRunProject, "dry-run", false, "Validate inputs and show what would run without generating the project model") projectCmd.Flags().StringVar(&ProjectLogFile, "log-file", "", "Path to the log file (default: /logs/.log)") } diff --git a/cli/cmd/prune.go b/cli/cmd/prune.go index a4da4e6b8..162a3a4c2 100644 --- a/cli/cmd/prune.go +++ b/cli/cmd/prune.go @@ -57,24 +57,23 @@ func resolveCategories() (utils.PruneCategory, error) { var pruneCmd = &cobra.Command{ Use: "prune", - Short: "Remove stale downloaded artifacts from ~/.opentaint", - Long: `Remove stale downloaded artifacts from the local cache (~/.opentaint). - -Identifies artifacts that are no longer needed: -- Old versions of analyzer JARs, autobuilder JARs, and rules -- Downloaded JDK/JRE versions that don't match the current version -- Cached project models - -Use category flags to prune selectively: - --artifacts Stale analyzer and autobuilder JARs - --rules Stale rules directories - --jdk Old JDK/JRE versions - --models Cached project models - --logs Project log files - --install Install-tier lib and JRE artifacts (requires re-download) - -Without category flags, prunes: artifacts + rules + jdk + models. -With --all: prunes everything including logs and install-tier.`, + Short: "Remove stale downloaded artifacts from the cache", + Long: `Remove stale downloaded artifacts from the local cache (~/.opentaint): superseded analyzer and autobuilder JARs, old rules, JDK and JRE versions that no longer match the configured one, and cached project models. + +Select categories with --artifacts, --rules, --jdk, --models, --logs, or --install. Without a category flag, prune removes artifacts, rules, jdk, and models; --all removes everything, including logs and install-tier artifacts, and cannot be combined with a specific category flag. + +Preview the deletions with --dry-run, and skip the confirmation prompt with --yes. Restore install-tier artifacts afterward with opentaint pull.`, + Example: ` # Prune the default categories after confirming + opentaint prune + + # Prune only old JDK and JRE versions + opentaint prune --jdk + + # Prune everything, including logs and install-tier artifacts + opentaint prune --all + + # Preview what would be deleted without deleting + opentaint prune --dry-run`, Run: func(cmd *cobra.Command, args []string) { categories, err := resolveCategories() if err != nil { @@ -84,23 +83,23 @@ With --all: prunes everything including logs and install-tier.`, // Acquire global prune lock pruneLockPath, err := utils.PruneLockPath() if err != nil { - out.Fatalf("Failed to resolve prune lock path: %s", err) + failf("Failed to resolve prune lock path: %s", err) } pruneLock, err := utils.TryLockExclusive(pruneLockPath, utils.LockMeta{ PID: os.Getpid(), Command: "prune", }) if err == utils.ErrLocked { - out.Fatal("Another prune is already running") + failWith(1, "Another prune is already running") } if err != nil { - out.Fatalf("Failed to acquire prune lock: %s", err) + failf("Failed to acquire prune lock: %s", err) } defer pruneLock.Unlock() result, err := utils.ScanForStaleArtifacts(categories) if err != nil { - out.Fatalf("Failed to scan for stale artifacts: %s", err) + failf("Failed to scan for stale artifacts: %s", err) } // Display skipped projects @@ -130,22 +129,27 @@ With --all: prunes everything including logs and install-tier.`, Render() if pruneDryRun { - out.Print("Dry run mode. No files were deleted.") + out.Print("Dry run complete. No files were deleted.") + suggest("To delete these artifacts, run:", withFlag(rerunWithoutDryRun(), "--yes")) return } if !pruneYes { if !out.Confirm("Delete these artifacts?", false) { out.Print("Prune cancelled.") + suggest("To prune without confirming, run:", withFlag(rerunWithoutDryRun(), "--yes")) return } } if err := utils.DeleteArtifacts(result.Stale); err != nil { - out.Fatalf("Failed to delete artifacts: %s", err) + failf("Failed to delete artifacts: %s", err) } out.Successf("Pruned %d items, freed %s", result.TotalCount, output.FormatSize(result.TotalSize)) + if pruneInstall || pruneAll { + suggest("To restore the removed components, run:", "opentaint pull") + } }, } diff --git a/cli/cmd/pull.go b/cli/cmd/pull.go index ff95db860..d73b5fa38 100644 --- a/cli/cmd/pull.go +++ b/cli/cmd/pull.go @@ -17,15 +17,17 @@ import ( var pullCmd = &cobra.Command{ Use: "pull", - Short: "Download autobuilder, analyzer binaries, rules and Java runtime", - Long: `Download all necessary binaries and assets: -- OpenTaint autobuilder JAR -- OpenTaint analyzer JAR -- OpenTaint rules archive -- Java runtime (Temurin JRE) - -This prepares the environment with all required dependencies for offline analysis. -When bundled artifacts are present (from a release archive), they will be used directly.`, + Short: "Download the analysis toolchain and Java runtime", + Long: `Download the analyzer, autobuilder, built-in rules, go-ssa-server, and a bundled Java runtime into the local cache. OpenTaint uses these to build and analyze projects without further network access. + +When bundled artifacts from a release archive are present, they are used directly instead of downloading. + +Run opentaint pull once before your first scan. Remove stale downloads later with opentaint prune.`, + Example: ` # Download the toolchain before the first scan + opentaint pull + + # Fetch a different Java runtime version + opentaint pull --java-version 17`, Run: func(cmd *cobra.Command, args []string) { out.Section("OpenTaint Pull"). Field("Autobuilder", globals.Config.Autobuilder.Version). @@ -40,7 +42,7 @@ When bundled artifacts are present (from a release archive), they will be used d installCurrent := utils.IsInstallCurrent() if !installCurrent { if err := utils.CleanInstallDir(); err != nil { - out.Fatalf("Failed to clean install directory: %s", err) + failf("Failed to clean install directory: %s", err) } } @@ -50,26 +52,29 @@ When bundled artifacts are present (from a release archive), they will be used d for _, spec := range artifacts { node, err := downloadArtifact(spec, installNextToBinary, installCurrent) if err != nil { - out.Fatalf("Failed to download %s: %s", spec.Kind(), err) + failf("Failed to download %s: %s", spec.Kind(), err) } summaryNodes = append(summaryNodes, node) } javaNode, err := downloadJava(installNextToBinary, installCurrent) if err != nil { - out.Fatalf("Failed to download Java: %s", err) + failf("Failed to download Java: %s", err) } summaryNodes = append(summaryNodes, javaNode) // Write version marker after all downloads succeed if err := utils.WriteInstallVersionMarker(); err != nil { - out.Fatalf("Failed to write install version marker: %s", err) + failf("Failed to write install version marker: %s", err) } out.Blank() out.Section("Pull Summary"). Child(summaryNodes...). Render() + + out.Successf("Pull completed.") + suggest("To scan your project, run:", "opentaint scan .") }, } diff --git a/cli/cmd/rerun_test.go b/cli/cmd/rerun_test.go new file mode 100644 index 000000000..310b263b6 --- /dev/null +++ b/cli/cmd/rerun_test.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "os" + "testing" + + "github.com/seqra/opentaint/internal/analyzer" +) + +func withOSArgs(t *testing.T, args []string) { + t.Helper() + saved := os.Args + os.Args = args + t.Cleanup(func() { os.Args = saved }) +} + +func TestRerunWithoutDryRunStripsFlag(t *testing.T) { + withOSArgs(t, []string{"/usr/bin/opentaint", "scan", "./proj", "--dry-run", "--color", "never"}) + got := rerunWithoutDryRun() + want := "opentaint scan ./proj --color never" + if got != want { + t.Fatalf("rerunWithoutDryRun() = %q, want %q", got, want) + } +} + +func TestRerunWithoutDryRunStripsEqualsForm(t *testing.T) { + withOSArgs(t, []string{"opentaint", "compile", ".", "--dry-run=true", "-o", "./model"}) + got := rerunWithoutDryRun() + want := "opentaint compile . -o ./model" + if got != want { + t.Fatalf("rerunWithoutDryRun() = %q, want %q", got, want) + } +} + +func TestRerunWithoutDryRunQuotesSpaces(t *testing.T) { + withOSArgs(t, []string{"opentaint", "scan", "my project", "--dry-run"}) + got := rerunWithoutDryRun() + want := "opentaint scan 'my project'" + if got != want { + t.Fatalf("rerunWithoutDryRun() = %q, want %q", got, want) + } +} + +func TestWithFlag(t *testing.T) { + if got := withFlag("opentaint prune", "--yes"); got != "opentaint prune --yes" { + t.Fatalf("withFlag append = %q", got) + } + if got := withFlag("opentaint prune --yes", "--yes"); got != "opentaint prune --yes" { + t.Fatalf("withFlag no-op = %q", got) + } +} + +func TestRerunReplacingFlagValueForm(t *testing.T) { + withOSArgs(t, []string{"opentaint", "scan", ".", "--max-memory", "8G"}) + got := rerunReplacingFlag("16G", "--max-memory") + want := "opentaint scan . --max-memory 16G" + if got != want { + t.Fatalf("rerunReplacingFlag() = %q, want %q", got, want) + } +} + +func TestRerunReplacingFlagAliasAndEqualsForm(t *testing.T) { + withOSArgs(t, []string{"opentaint", "scan", ".", "-t", "15m", "--timeout=10m"}) + got := rerunReplacingFlag("30m0s", "--timeout", "-t") + want := "opentaint scan . --timeout 30m0s" + if got != want { + t.Fatalf("rerunReplacingFlag() = %q, want %q", got, want) + } +} + +func TestRerunReplacingFlagAppendsWhenAbsent(t *testing.T) { + withOSArgs(t, []string{"opentaint", "test", "rule", "run", "./model"}) + got := rerunReplacingFlag("16G", "--max-memory") + want := "opentaint test rule run ./model --max-memory 16G" + if got != want { + t.Fatalf("rerunReplacingFlag() = %q, want %q", got, want) + } +} + +func TestDoubleMemory(t *testing.T) { + cases := map[string]string{ + "8G": "16G", + "1024m": "2048m", + "83886080": "167772160", + "weird": "16G", + "": "16G", + } + for in, want := range cases { + if got := doubleMemory(in); got != want { + t.Fatalf("doubleMemory(%q) = %q, want %q", in, got, want) + } + } +} + +func TestRetrySuggestion(t *testing.T) { + withOSArgs(t, []string{"opentaint", "scan", ".", "--max-memory", "8G"}) + + oom, ok := retrySuggestion(analyzer.ExitOOM, 900e9, "8G") + if !ok || oom.Description != "To retry with more memory, run:" || oom.Command != "opentaint scan . --max-memory 16G" { + t.Fatalf("OOM retry = %+v ok=%t", oom, ok) + } + + timeoutRetry, ok := retrySuggestion(analyzer.ExitTimeout, 900e9, "8G") + if !ok || timeoutRetry.Description != "To retry with a longer timeout, run:" { + t.Fatalf("timeout retry = %+v ok=%t", timeoutRetry, ok) + } + want := "opentaint scan . --max-memory 8G --timeout 30m0s" + if timeoutRetry.Command != want { + t.Fatalf("timeout retry command = %q, want %q", timeoutRetry.Command, want) + } + + if _, ok := retrySuggestion(analyzer.ExitException, 900e9, "8G"); ok { + t.Fatal("exception exit code must not produce a retry suggestion") + } +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 214028ec8..85a223926 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -33,9 +33,11 @@ var updateHintCh = make(chan string, 1) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "opentaint", - Short: "OpenTaint Analyzer", - Long: `OpenTaint is a CLI tool that analyzes Java and Kotlin projects to find vulnerabilities`, + Use: "opentaint", + Short: "Find vulnerabilities in source code with taint analysis", + Long: `OpenTaint finds vulnerabilities by tracing tainted data from untrusted sources to sensitive sinks. Java, Kotlin, and Go projects are supported. + +Run opentaint pull once to fetch the toolchain, opentaint scan to analyze a project, and opentaint summary to re-inspect a SARIF report.`, SilenceErrors: true, SilenceUsage: true, diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 9750dcdeb..8c88b4374 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "github.com/seqra/opentaint/internal/analyzer" @@ -72,15 +73,31 @@ func (p scanPlan) title() string { // scanCmd represents the scan command var scanCmd = &cobra.Command{ Use: "scan [source-path]", - Short: "Scan your Java or Kotlin project", + Short: "Scan a project for vulnerabilities", Args: cobra.MaximumNArgs(1), - Long: `This command automatically detects Java/Kotlin build systems, builds the project, and analyzes it + Long: `Scan a project for vulnerabilities. OpenTaint detects the build system, builds the project, and runs taint analysis over the result. -Arguments: - source-path - Path to the project sources (default: current directory) +The optional source-path argument is the project root and defaults to the current directory. Pass --project-model to analyze a pre-compiled project model instead of building; source-path and --project-model are mutually exclusive. -Use --project-model to scan a pre-compiled project model instead of compiling from sources. -`, +Findings are written as a SARIF report to --output, or into the project model directory when unset, and summarized on completion. + +Run opentaint pull once before your first scan to fetch the toolchain. Re-inspect a report later with opentaint summary. + +` + scanExitCodesHelp("Scan completed"), + Example: ` # Scan the current directory with the built-in rules + opentaint scan . + + # Scan a project and write a SARIF report + opentaint scan ./my-app -o report.sarif + + # Analyze a pre-compiled project model instead of building + opentaint scan --project-model ./model -o report.sarif + + # Run a custom ruleset and report only errors + opentaint scan . --ruleset ./rules --severity error -o report.sarif + + # Give a large project more time and memory + opentaint scan . --timeout 30m --max-memory 16G -o report.sarif`, Annotations: map[string]string{"PrintConfig": "true"}, Run: func(cmd *cobra.Command, args []string) { if scanFlags.DebugRunAnalysisOnSelectedEntryPoints != "" { @@ -93,7 +110,7 @@ Use --project-model to scan a pre-compiled project model instead of compiling fr func prepareScanConfig(cfg ScanConfig, args []string) ScanConfig { if len(args) > 0 && cfg.ProjectModelPath != "" { out.Error("Cannot use both a source path argument and --project-model flag") - suggest("Use either a source path or --project-model", + suggest("Use either a source path or --project-model:", utils.NewScanCommand("").Build()+"\n "+utils.NewScanCommand("").WithProjectModel("").Build()) os.Exit(1) } @@ -122,19 +139,19 @@ func addEntryPointsFlag(cmd *cobra.Command) { } func addRuleIDFlag(cmd *cobra.Command) { - cmd.Flags().StringArrayVar(&scanFlags.RuleID, "rule-id", nil, "Filter active rules by ID (repeatable)") + cmd.Flags().StringArrayVar(&scanFlags.RuleID, "rule-id", nil, "Run only rules with this ID (repeatable)") } func addScanFlags(cmd *cobra.Command) { - cmd.Flags().DurationVarP(&globals.Config.Scan.Timeout, "timeout", "t", 900*time.Second, "Timeout for analysis") + cmd.Flags().DurationVarP(&globals.Config.Scan.Timeout, "timeout", "t", 900*time.Second, "Maximum wall-clock time for analysis (e.g. 30m, 1h)") - cmd.Flags().StringArrayVar(&scanFlags.Ruleset, "ruleset", []string{"builtin"}, "YAML rules file, directory of YAML rules files ending in .yml or .yaml, or `builtin` to scan with built-in rules") + cmd.Flags().StringArrayVar(&scanFlags.Ruleset, "ruleset", []string{"builtin"}, "Rules to run: a YAML file, a directory of .yml or .yaml files, or builtin for the built-in rules (repeatable)") - cmd.Flags().BoolVar(&scanFlags.SemgrepCompatibilitySarif, "semgrep-compatibility-sarif", true, "Use Semgrep compatible ruleId") - cmd.Flags().StringVarP(&scanFlags.SarifReportPath, "output", "o", "", "Path to the SARIF-report output file") + cmd.Flags().BoolVar(&scanFlags.SemgrepCompatibilitySarif, "semgrep-compatibility-sarif", true, "Use Semgrep-compatible rule IDs in the SARIF report") + cmd.Flags().StringVarP(&scanFlags.SarifReportPath, "output", "o", "", "Path to write the SARIF report") - cmd.Flags().StringArrayVar(&scanFlags.Severity, "severity", []string{"warning", "error"}, "Report findings only from rules matching the supplied severity level. By default only warning and error rules are run (note, warning, error)") - cmd.Flags().StringVar(&globals.Config.Scan.MaxMemory, "max-memory", "8G", "Maximum memory for the analyzer (e.g., 1024m, 8G, 81920k, 83886080)") + cmd.Flags().StringArrayVar(&scanFlags.Severity, "severity", []string{"warning", "error"}, "Run only rules at these severity levels: note, warning, error (repeatable)") + cmd.Flags().StringVar(&globals.Config.Scan.MaxMemory, "max-memory", "8G", "Maximum analyzer heap size (e.g. 8G, 1024m)") cmd.Flags().Int64Var(&globals.Config.Scan.CodeFlowLimit, "code-flow-limit", 0, "Maximum number of code flows to include in the report (0 = unlimited)") cmd.Flags().BoolVar(&scanFlags.DryRun, "dry-run", false, "Validate inputs and show what would run without compiling or scanning") cmd.Flags().BoolVar(&scanFlags.Recompile, "recompile", false, "Force recompilation even if a cached project model exists") @@ -143,7 +160,7 @@ func addScanFlags(cmd *cobra.Command) { cmd.Flags().StringArrayVar(&scanFlags.PassthroughApproximations, "passthrough-approximations", nil, "Pass-through approximation YAML file or directory (repeatable)") - cmd.Flags().StringArrayVar(&scanFlags.DataflowApproximations, "dataflow-approximations", nil, "Dataflow approximation class directory or Java source directory (repeatable)") + cmd.Flags().StringArrayVar(&scanFlags.DataflowApproximations, "dataflow-approximations", nil, "Dataflow approximation class directory or Java source directory (Java analysis only, repeatable)") cmd.Flags().BoolVar(&scanFlags.TrackExternalMethods, "track-external-methods", false, "Write external-method coverage files next to the SARIF report") } @@ -172,7 +189,7 @@ func isDefaultSeverity(sev []string) bool { // dockerScanSuggestion builds the "try Docker-based scan" fallback hint. func dockerScanSuggestion(cfg ScanConfig, projectRoot, sarifReportPath string) output.Suggestion { return output.Suggestion{ - Description: dockerFallbackHintPrefix + "scan:", + Description: "If the required Java is missing, set JAVA_HOME or scan in a container instead:", Command: utils.BuildScanCommandWithDocker(currentScanBuilder(cfg, ""), projectRoot, sarifReportPath, cfg.Ruleset), } } @@ -190,7 +207,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { if err := validation.ValidateSourceProject(absUserProjectRoot); err != nil { if validation.IsProjectModel(absUserProjectRoot) { out.ErrorErr(err) - suggest("Use --project-model to scan a pre-compiled model", currentScanBuilder(cfg, "").WithProjectModel(absUserProjectRoot).Build()) + suggest("Use --project-model to scan a pre-compiled model:", currentScanBuilder(cfg, "").WithProjectModel(absUserProjectRoot).Build()) os.Exit(1) } out.FatalErr(err) @@ -283,7 +300,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { } if cfg.DryRun { - runDryRun("Compilation and analysis") + runDryRun("the build and scan") return } @@ -296,25 +313,25 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { } if hasBuiltin { if _, err := utils.EnsureRulesPath(out); err != nil { - out.Fatalf("Failed to prepare built-in rules: %s", err) + failf("Failed to prepare built-in rules: %s", err) } } if plan.needsCompilation { autobuilderJarPath, err := ensureAutobuilderAvailable() if err != nil { - out.Fatalf("Native compile preparation failed: %s", err) + failf("Native compile preparation failed: %s", err) } compileJavaRunner := newAutobuilderJavaRunner() if _, err := compileJavaRunner.EnsureJava(); err != nil { - out.Fatalf("Failed to resolve Java for compilation: %s", err) + failf("Failed to resolve Java for compilation: %s", err) } // Wipe any residue from a prior crashed compile before writing new output. if plan.projectCachePath != "" { if err := os.RemoveAll(plan.absProjectModel); err != nil { - out.Fatalf("Failed to prepare cache directory: %s", err) + failf("Failed to prepare cache directory: %s", err) } } @@ -333,7 +350,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { if plan.projectCachePath != "" { if err := utils.MarkCompileComplete(plan.projectCachePath); err != nil { _ = os.RemoveAll(plan.absProjectModel) - out.Fatalf("Failed to mark model complete: %s", err) + failf("Failed to mark model complete: %s", err) } if err := plan.cacheLock.Downgrade(); err != nil { output.LogInfof("Cache lock downgrade failed, continuing under exclusive: %v", err) @@ -344,7 +361,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { } if err := utils.EnsureParentDir(absSarifReportPath); err != nil { - out.Fatalf("Failed to create output directory: %s", err) + failf("Failed to create output directory: %s", err) } // Update builder with native paths for native execution @@ -397,7 +414,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { analyzerJarPath, err := ensureAnalyzerAvailable() if err != nil { - out.Fatalf("Native scan preparation failed: %s", err) + failf("Native scan preparation failed: %s", err) } nativeBuilder.SetJarPath(analyzerJarPath) @@ -406,7 +423,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { analyzerJavaRunner := newAnalyzerJavaRunner() if _, err := analyzerJavaRunner.EnsureJava(); err != nil { - out.Fatalf("Failed to resolve Java for analyzer: %s", err) + failf("Failed to resolve Java for analyzer: %s", err) } var analyzerFail *analyzer.Error @@ -461,15 +478,43 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { var suggestions []output.Suggestion if analyzerFail != nil { suggestions = appendLogSuggestion(suggestions) + if retry, ok := retrySuggestion(analyzerFail.ExitCode, globals.Config.Scan.Timeout, globals.Config.Scan.MaxMemory); ok { + suggestions = append(suggestions, retry) + } } if report != nil { // Scan does not expose summary's filter/group flags, so pass zero values: // no filtering, default group dimension, first-flow code-flow selection. printSarifSummary(report, absSarifReportPath, sarif.Filters{}, sarif.ListingOptions{MaxNestingLevel: -1}) - suggestions = append(suggestions, output.Suggestion{ - Description: "To view findings run", - Command: utils.NewSummaryCommand(absSarifReportPath).WithShowFindings().Build(), - }) + switch { + case cfg.DebugFactReachabilitySarif: + if analyzerFail == nil { + out.Successf("Reachability analysis completed.") + } + // The reachability report is the command's deliverable; point at it, + // never at the main SARIF. + reachabilityReportPath := filepath.Join(filepath.Dir(absSarifReportPath), "debug-ifds-fact-reachability.sarif") + suggestions = append(suggestions, output.Suggestion{ + Description: "To view the reachability report, run:", + Command: utils.NewSummaryCommand(reachabilityReportPath).WithShowFindings().Build(), + }) + case sarif.GenerateSummary(report).TotalFindings > 0: + if analyzerFail == nil { + out.Successf("Scan completed.") + } + suggestions = append(suggestions, output.Suggestion{ + Description: "To view the findings, run:", + Command: utils.NewSummaryCommand(absSarifReportPath).WithShowFindings().Build(), + }) + case analyzerFail == nil: + out.Successf("Scan completed. No vulnerabilities found at %s severity.", strings.Join(cfg.Severity, " or ")) + if isDefaultSeverity(cfg.Severity) { + suggestions = append(suggestions, output.Suggestion{ + Description: "To also check note-level rules, run:", + Command: noteSeverityScanCommand(cfg), + }) + } + } } out.Suggestions(suggestions...) @@ -478,6 +523,20 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { } } +// noteSeverityScanCommand builds the follow-up command for a clean scan: the +// same invocation narrowed to the note-level rules the default run skips. +func noteSeverityScanCommand(cfg ScanConfig) string { + sourcePath := cfg.UserProjectPath + if cfg.ProjectModelPath != "" { + sourcePath = "" + } + b := currentScanBuilder(cfg, sourcePath).WithSeverity([]string{"note"}) + if cfg.ProjectModelPath != "" { + b.WithProjectModel(cfg.ProjectModelPath) + } + return b.Build() +} + func resolveScanPlan(cfg ScanConfig, absUserProjectRoot string) scanPlan { if cfg.ProjectModelPath != "" { return scanPlan{ @@ -537,7 +596,7 @@ func resolveScanPlan(cfg ScanConfig, absUserProjectRoot string) scanPlan { } else { out.Error("Another scan is currently analyzing this project") } - suggest("To scan an existing model instead", utils.NewScanCommand("").WithProjectModel("").Build()) + suggest("To scan an existing model instead, run:", utils.NewScanCommand("").WithProjectModel("").Build()) os.Exit(1) } if lockErr != nil { diff --git a/cli/cmd/suggest.go b/cli/cmd/suggest.go index 9508a86cb..416c76e69 100644 --- a/cli/cmd/suggest.go +++ b/cli/cmd/suggest.go @@ -3,20 +3,97 @@ package cmd import ( "fmt" "os" + "strconv" + "strings" + "time" + "github.com/seqra/opentaint/internal/analyzer" "github.com/seqra/opentaint/internal/globals" "github.com/seqra/opentaint/internal/output" ) -// dockerFallbackHintPrefix is the shared lead-in for the Docker-based fallback -// hints emitted when native compilation can't find a suitable Java. compile and -// scan complete it with their respective "compilation:" / "scan:" suffix. -const dockerFallbackHintPrefix = "If native compilation fails due to missing required Java, set JAVA_HOME according to the project's requirements or try Docker-based " - func suggest(description, command string) { out.Suggest(description, command) } +// withFlag appends flag to the command string when it is not already present, +// for suggestions that re-run the current invocation with one extra flag. +func withFlag(command, flag string) string { + if strings.Contains(command, flag) { + return command + } + return command + " " + flag +} + +// retrySuggestion builds the "re-run with more resources" hint for a resource +// analyzer failure. The second result is false for exit codes where a plain +// retry would not help (unhandled exception, configuration error). +func retrySuggestion(exitCode int, timeout time.Duration, maxMemory string) (output.Suggestion, bool) { + switch exitCode { + case analyzer.ExitOOM: + return output.Suggestion{ + Description: "To retry with more memory, run:", + Command: rerunReplacingFlag(doubleMemory(maxMemory), "--max-memory"), + }, true + case analyzer.ExitTimeout: + return output.Suggestion{ + Description: "To retry with a longer timeout, run:", + Command: rerunReplacingFlag((timeout * 2).String(), "--timeout", "-t"), + }, true + } + return output.Suggestion{}, false +} + +// rerunReplacingFlag reconstructs the current invocation with the named flag +// (any alias, in both "--flag value" and "--flag=value" forms) replaced by the +// given value, appended as names[0]. +func rerunReplacingFlag(value string, names ...string) string { + args := []string{"opentaint"} + skipNext := false + for _, arg := range os.Args[1:] { + if skipNext { + skipNext = false + continue + } + matched := false + for _, name := range names { + if arg == name { + matched = true + skipNext = true + break + } + if strings.HasPrefix(arg, name+"=") { + matched = true + break + } + } + if matched { + continue + } + args = append(args, shellQuote(arg)) + } + args = append(args, names[0], shellQuote(value)) + return strings.Join(args, " ") +} + +// doubleMemory doubles a memory value like 8G or 1024m, falling back to the +// runtime failure message's own 16G example when the value does not parse. +func doubleMemory(value string) string { + digits := 0 + for digits < len(value) && value[digits] >= '0' && value[digits] <= '9' { + digits++ + } + suffix := value[digits:] + if digits == 0 || len(suffix) > 1 { + return "16G" + } + n, err := strconv.ParseInt(value[:digits], 10, 64) + if err != nil { + return "16G" + } + return fmt.Sprintf("%d%s", n*2, suffix) +} + // logSuggestion returns a Suggestion pointing at the active log file. The // second result is false when no log file is active (e.g. failures that occur // before logging is activated), in which case callers omit it. diff --git a/cli/cmd/summary.go b/cli/cmd/summary.go index 0959b5257..58231fa4e 100644 --- a/cli/cmd/summary.go +++ b/cli/cmd/summary.go @@ -9,14 +9,30 @@ import ( // summaryCmd represents the summary command var summaryCmd = &cobra.Command{ - Use: "summary sarif", - Short: "Print summary of your sarif", + Use: "summary ", + Short: "Summarize a SARIF report", Args: cobra.ExactArgs(1), // require exactly one argument - Long: `Print summary of your sarif file + Long: `Summarize a SARIF report on the terminal. OpenTaint counts the findings by severity, groups them, and shows which rules ran and which produced results. -Arguments: - sarif - Path to a sarif file -`, +The required positional argument is the path to a SARIF report, such as one written by opentaint scan or opentaint test. Pass --show-findings to list every finding; narrow the listing with --severity, --rule-id, or --path, and expand code flows with --show-code-snippets and --verbose-flow. + +The report is read only: the summary and any findings are printed to the terminal and nothing is written to disk. + +Run opentaint scan to produce the report this command reads.`, + Example: ` # Print a summary of a report + opentaint summary report.sarif + + # List every finding with its location + opentaint summary report.sarif --show-findings + + # Show only error-level findings + opentaint summary report.sarif --show-findings --severity error + + # Group the listing by rule + opentaint summary report.sarif --show-findings --group-by rule-id + + # Trace one rule with full code flow and snippets + opentaint summary report.sarif --show-findings --rule-id --show-code-snippets --verbose-flow`, Run: func(cmd *cobra.Command, args []string) { for _, s := range summarySeverities { @@ -39,6 +55,13 @@ Arguments: out.Fatalf("Failed to load SARIF report: %s", err) } printSarifSummary(report, absSarifPath, summaryFilters(), summaryListingOptions(dim, codeFlowSel)) + + if !showFindings && sarif.GenerateSummary(report.Filter(summaryFilters())).TotalFindings > 0 { + out.Suggest( + "To list the findings, run:", + currentSummaryBuilder(absSarifPath).WithShowFindings().Build(), + ) + } }, } @@ -58,16 +81,16 @@ var summaryCodeFlow string func init() { rootCmd.AddCommand(summaryCmd) - summaryCmd.Flags().BoolVar(&showFindings, "show-findings", false, "Show all issues from Sarif file") + summaryCmd.Flags().BoolVar(&showFindings, "show-findings", false, "Show every finding in the SARIF report") summaryCmd.Flags().BoolVar(&showCodeSnippets, "show-code-snippets", false, "Show finding related code snippets") summaryCmd.Flags().BoolVar(&verboseFlow, "verbose-flow", false, "Show full code flow steps for findings") summaryCmd.Flags().StringArrayVar(&summaryPaths, "path", nil, "Show only findings whose file path matches this glob (** supported, repeatable)") - summaryCmd.Flags().StringArrayVar(&summarySeverities, "severity", nil, "Show only findings of this SARIF level: error, warning, note, none (repeatable)") - summaryCmd.Flags().StringArrayVar(&summaryRuleIDs, "rule-id", nil, "Show only findings for this rule: full id, leaf name, or glob (repeatable)") + summaryCmd.Flags().StringArrayVar(&summarySeverities, "severity", nil, "Show only findings at these SARIF levels: note, warning, error, none (repeatable)") + summaryCmd.Flags().StringArrayVar(&summaryRuleIDs, "rule-id", nil, "Show only findings from this rule: full id, leaf name, or glob (repeatable)") summaryCmd.Flags().StringArrayVar(&summaryFingerprints, "partial-fingerprint", nil, "Show only findings whose partial fingerprint starts with this value (git-hash style, repeatable)") - summaryCmd.Flags().StringVar(&summaryFingerprintKey, "partial-fingerprint-key", "", "partialFingerprints key matched by --partial-fingerprint (default vulnerabilityWithTraceHash/v1)") + summaryCmd.Flags().StringVar(&summaryFingerprintKey, "partial-fingerprint-key", "", "partialFingerprints key matched by --partial-fingerprint; defaults to vulnerabilityWithTraceHash/v1") summaryCmd.Flags().IntVar(&summaryMaxNestingLevel, "max-nesting-level", -1, "Collapse code-flow steps deeper than this call-nesting level (-1 = no cap)") - summaryCmd.Flags().StringVar(&summaryGroupBy, "group-by", "", "Group the --show-findings listing by: severity, rule-id, file-path (default file-path)") + summaryCmd.Flags().StringVar(&summaryGroupBy, "group-by", "", "Group the --show-findings listing by: severity, rule-id, file-path; defaults to file-path") summaryCmd.Flags().StringVar(&summaryCodeFlow, "code-flow", "", "Render code flows: \"all\", a 1-based index, or unset (first only)") } @@ -137,7 +160,7 @@ func printSarifSummary(report *sarif.Report, absSarifPath string, filters sarif. if showFindings && hasOmittedFlow && !verboseFlow { out.Suggest( - "To see full code flow and code snippets, use:", + "To see the full code flow and code snippets, run:", currentSummaryBuilder(absSarifPath).WithVerboseFlow().WithShowCodeSnippets().Build(), ) } diff --git a/cli/cmd/test.go b/cli/cmd/test.go index 409240606..8c815e022 100644 --- a/cli/cmd/test.go +++ b/cli/cmd/test.go @@ -9,17 +9,25 @@ import ( var testCmd = &cobra.Command{ Use: "test", Short: "Create and run rule and approximation tests", - Long: `Tools for creating test projects, running annotated rule and approximation tests, and debugging rule reachability.`, + Long: `Create, run, and debug rule and approximation tests. Rule tests check detection rules against annotated sample projects; approximation tests check dataflow approximations the same way. + +Scaffold a project with init, compile it with opentaint compile, then run the samples with test rule run or test approximation run. Use test rule reachability to debug why a single rule does or does not fire.`, } var testRuleCmd = &cobra.Command{ Use: "rule", Short: "Create, run, and debug detection-rule tests", + Long: `Create, run, and debug taint detection-rule tests. Rule tests check that a rule fires on positive samples and stays silent on negative ones. + +Scaffold a test project with test rule init, compile it with opentaint compile, then run the samples with test rule run. Use test rule reachability to trace why a single rule does or does not fire.`, } var testApproximationCmd = &cobra.Command{ Use: "approximation", Short: "Create and run dataflow-approximation tests", + Long: `Create and run dataflow-approximation tests. Approximation tests check that a dataflow approximation carries taint from source to sink across your samples. + +Scaffold a test project with test approximation init, compile it with opentaint compile, then run the samples with test approximation run, supplying the approximation under test with --dataflow-approximations.`, } func init() { @@ -28,20 +36,9 @@ func init() { testCmd.AddCommand(testApproximationCmd) } -func testExitCodesHelp(passedLine string) string { - return `Exit codes: - 0 ` + passedLine + ` - 1 General failure (configuration or infrastructure error) - 2 One or more tests failed (false negatives/positives or skipped samples) - 252 Unhandled analyzer exception - 253 Out of memory (try increasing --max-memory) - 254 Analysis timed out (try increasing --timeout) - 255 Project configuration error` -} - func addTestRunFlags(cmd *cobra.Command, outputDir *string, timeout *time.Duration, maxMemory *string, dataflow *[]string) { cmd.Flags().StringVarP(outputDir, "output", "o", "", "Directory for test-result.json and test-results.sarif") - cmd.Flags().DurationVar(timeout, "timeout", 600*time.Second, "Analysis timeout") - cmd.Flags().StringVar(maxMemory, "max-memory", "8G", "Maximum analyzer heap size (e.g., 8G)") - cmd.Flags().StringArrayVar(dataflow, "dataflow-approximations", nil, "Dataflow approximation class directory or Java source directory (repeatable)") + cmd.Flags().DurationVar(timeout, "timeout", 600*time.Second, "Maximum wall-clock time for analysis (e.g. 30m, 1h)") + cmd.Flags().StringVar(maxMemory, "max-memory", "8G", "Maximum analyzer heap size (e.g. 8G, 1024m)") + cmd.Flags().StringArrayVar(dataflow, "dataflow-approximations", nil, "Dataflow approximation class directory or Java source directory (Java analysis only, repeatable)") } diff --git a/cli/cmd/test_approximation_run.go b/cli/cmd/test_approximation_run.go index 018e16c03..ca0210e9f 100644 --- a/cli/cmd/test_approximation_run.go +++ b/cli/cmd/test_approximation_run.go @@ -17,13 +17,21 @@ var ( var testApproximationRunCmd = &cobra.Command{ Use: "run ", - Short: "Run dataflow approximation tests on a compiled project model", - Long: `Run the samples specified in rule-test.yaml with the supplied dataflow approximations applied. + Short: "Run dataflow-approximation tests on a compiled project model", + Long: `Run the samples declared in rule-test.yaml with the supplied dataflow approximations applied and report which passed. A fixed source-to-sink harness rule is applied automatically; positive samples reference it by id approximation-rule. -A built-in source-to-sink harness rule is applied automatically; positive samples reference the -approximation-rule.yaml rule with id "approximation-rule". +The project-model argument is a compiled project model directory, produced by opentaint compile. Supply the approximation under test with --dataflow-approximations. + +Results are written as test-result.json and a test-results.sarif report to --output, or to a temporary directory when unset. + +Compile the test project with opentaint compile before running. Inspect the results afterward with opentaint summary. ` + testExitCodesHelp("All approximation tests passed"), + Example: ` # Run an approximation test against a compiled model + opentaint test approximation run ./approx-test/model --dataflow-approximations ./approx + + # Write results to a directory + opentaint test approximation run ./approx-test/model --dataflow-approximations ./approx -o ./results`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { ruleDir, err := os.MkdirTemp("", "opentaint-approx-rule-*") @@ -36,6 +44,7 @@ approximation-rule.yaml rule with id "approximation-rule". runTestProject(args[0], testProjectOptions{ label: "Approximation tests", + passedLine: "All approximation tests passed.", tempDir: "opentaint-test-approximations-*", rulesets: []string{ruleDir}, outputDir: testApproxOutputDir, diff --git a/cli/cmd/test_init.go b/cli/cmd/test_init.go index 2c4966f82..a14105bc6 100644 --- a/cli/cmd/test_init.go +++ b/cli/cmd/test_init.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" + "github.com/seqra/opentaint/internal/output" "github.com/seqra/opentaint/internal/testapprox" "github.com/seqra/opentaint/internal/testproject" "github.com/seqra/opentaint/internal/testrule" @@ -18,19 +19,21 @@ var initRuleSourcesOnly bool var testRuleInitCmd = &cobra.Command{ Use: "init ", Short: "Create rule test projects with source and sink harnesses", - Long: `Create one or two Gradle test projects under . The sinks -project tests sink rules against a generic Taint source; the sources project -tests source rules against a generic Taint sink. Use --sinks-only or ---sources-only when only one project is needed. + Long: `Create one or two Gradle test projects for detection-rule tests. The sinks project tests sink rules against a generic taint source; the sources project tests source rules against a generic taint sink. -Each project includes: - - build.gradle.kts with compile-only dependencies, settings.gradle.kts - - src/main/java/test/ with Taint.java (the generic source()/sink()) for test sample sources - - test-rules/java/lib/test/generic-{source,sink}.yaml marker rules for test-only joins +The output-dir argument is the parent directory the projects are created under. By default both are scaffolded, as output-dir/sinks and output-dir/sources; pass --sinks-only or --sources-only to create just one. Use --dependency to add compile-only Maven dependencies for the samples. -Positive and negative samples are specified via rule-test.yaml. +Each project ships a rule-test.yaml where you declare the positive and negative samples, plus a Taint.java source and sink harness. -Use --dependency to add compile-only Maven dependencies for the samples.`, +After editing rule-test.yaml, compile the project with opentaint compile and run the samples with opentaint test rule run.`, + Example: ` # Scaffold both the sinks and sources test projects + opentaint test rule init ./rule-tests + + # Scaffold only the sinks project + opentaint test rule init ./rule-tests --sinks-only + + # Add a compile-only dependency for the samples + opentaint test rule init ./rule-tests --dependency `, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { if initRuleSinksOnly && initRuleSourcesOnly { @@ -50,28 +53,33 @@ Use --dependency to add compile-only Maven dependencies for the samples.`, if err := testrule.Scaffold(dir); err != nil { out.Fatalf("Failed to scaffold rule test project: %s", err) } - fmt.Printf("Rule test project (%s) initialized at %s\n", kind, dir) + out.Printf("Rule test project (%s) initialized at %s", kind, dir) } + dir := filepath.Join(args[0], kinds[0]) + modelDir := filepath.Join(dir, "model") + out.Suggestions( + output.Suggestion{Description: "To add your test samples, edit:", Command: filepath.Join(dir, "rule-test.yaml")}, + output.Suggestion{Description: "To compile the test project, run:", Command: fmt.Sprintf("opentaint compile %s -o %s", dir, modelDir)}, + output.Suggestion{Description: "To run the tests, run:", Command: fmt.Sprintf("opentaint test rule run %s", modelDir)}, + ) }, } var testApproximationInitCmd = &cobra.Command{ Use: "init ", - Short: "Create a dataflow approximation test project", - Long: `Create a minimal Gradle project for testing OpenTaint dataflow approximations. + Short: "Create a dataflow-approximation test project", + Long: `Create a Gradle test project for dataflow-approximation tests. The project pins a fixed source-to-sink rule that the samples are checked against. -The project includes: - - build.gradle.kts with compile-only dependencies - - settings.gradle.kts - - approximation-rule.yaml, the fixed source-to-sink rule the samples are checked against - - src/main/java/test/ with Taint.java (the fixed source() and sink()) for test sample sources +The output-dir argument is the directory the project is created in. Use --dependency to add compile-only Maven dependencies for the samples. The approximation under test is not baked in; supply it at run time with --dataflow-approximations. -Positive and negative samples are specified via rule-test.yaml. +The project ships a rule-test.yaml where you declare the positive and negative samples, plus a Taint.java source and sink and the fixed approximation-rule.yaml. -The approximation under test is supplied separately at test time with ---dataflow-approximations. +After editing rule-test.yaml, compile the project with opentaint compile and run the samples with opentaint test approximation run.`, + Example: ` # Scaffold an approximation test project + opentaint test approximation init ./approx-test -Use --dependency to add compile-only Maven dependencies for the samples.`, + # Add a compile-only dependency for the samples + opentaint test approximation init ./approx-test --dependency `, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { if err := testproject.Bootstrap(args[0], "approximation-test-project", initApproxProjectDeps); err != nil { @@ -80,7 +88,14 @@ Use --dependency to add compile-only Maven dependencies for the samples.`, if err := testapprox.Scaffold(args[0]); err != nil { out.Fatalf("Failed to scaffold approximation project: %s", err) } - fmt.Printf("Approximation test project initialized at %s\n", args[0]) + out.Printf("Approximation test project initialized at %s", args[0]) + dir := args[0] + modelDir := filepath.Join(dir, "model") + out.Suggestions( + output.Suggestion{Description: "To add your test samples, edit:", Command: filepath.Join(dir, "rule-test.yaml")}, + output.Suggestion{Description: "To compile the test project, run:", Command: fmt.Sprintf("opentaint compile %s -o %s", dir, modelDir)}, + output.Suggestion{Description: "To run the tests, run:", Command: fmt.Sprintf("opentaint test approximation run %s --dataflow-approximations ", modelDir)}, + ) }, } diff --git a/cli/cmd/test_rule_reachability.go b/cli/cmd/test_rule_reachability.go index ecb0bf323..58bfc6233 100644 --- a/cli/cmd/test_rule_reachability.go +++ b/cli/cmd/test_rule_reachability.go @@ -9,11 +9,26 @@ var reachabilityEntryPoint string var testRuleReachabilityCmd = &cobra.Command{ Use: "reachability [source-path]", Short: "Trace why a rule can or cannot reach its facts", - Long: `Scan a project with one rule and write a sibling fact-reachability SARIF -report (debug-ifds-fact-reachability.sarif) next to the main one. Use this to -debug why a rule does or does not fire. + Long: `Scan a project with a single rule and write a fact-reachability SARIF report so you can see why that rule does or does not fire. Referenced library source and sink rules are collected and analyzed automatically. -Referenced library source and sink rules are collected and analyzed automatically.`, +The rule-id argument selects the one rule to trace. The optional source-path argument is the project root and defaults to the current directory. Pass --project-model to trace a pre-compiled model instead; source-path and --project-model are mutually exclusive. Use --entry-points to start the analysis from a specific method. + +The report is written as debug-ifds-fact-reachability.sarif next to the main SARIF report. + +Run opentaint pull once before your first run to fetch the toolchain. Open the reachability report afterward with opentaint summary. + +` + scanExitCodesHelp("Reachability analysis completed"), + Example: ` # Trace a rule against the current directory + opentaint test rule reachability . + + # Trace a rule against a pre-compiled project model + opentaint test rule reachability --project-model ./model + + # Start the analysis from a specific entry-point method + opentaint test rule reachability . --entry-points com.example.App#main + + # Validate inputs without compiling or scanning + opentaint test rule reachability . --dry-run`, Annotations: map[string]string{"PrintConfig": "true"}, Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { diff --git a/cli/cmd/test_rule_run.go b/cli/cmd/test_rule_run.go index 4b380c617..83dbc3b6e 100644 --- a/cli/cmd/test_rule_run.go +++ b/cli/cmd/test_rule_run.go @@ -7,6 +7,7 @@ import ( "time" "github.com/seqra/opentaint/internal/analyzer" + "github.com/seqra/opentaint/internal/output" "github.com/seqra/opentaint/internal/utils" "github.com/seqra/opentaint/internal/utils/log" "github.com/spf13/cobra" @@ -25,14 +26,28 @@ var ( var testRuleRunCmd = &cobra.Command{ Use: "run ", Short: "Run detection-rule tests on a compiled project model", - Long: `Run detection rules against the samples specified in rule-test.yaml in the -compiled project model. + Long: `Run detection rules against the samples declared in rule-test.yaml and report which passed. The built-in rules are always included. + +The project-model argument is a compiled project model directory, produced by opentaint compile. Add your own rules with --ruleset, narrow the run to specific rules with --rule-id, and apply approximations with --dataflow-approximations or --passthrough-approximations. + +Results are written as test-result.json and a test-results.sarif report to --output, or to a temporary directory when unset. + +Compile the test project with opentaint compile before running. Inspect the results afterward with opentaint summary. ` + testExitCodesHelp("All rule tests passed"), + Example: ` # Run the built-in rules against a compiled model + opentaint test rule run ./rule-tests/sinks/model + + # Test a custom ruleset and write results to a directory + opentaint test rule run ./rule-tests/sinks/model --ruleset ./rules -o ./results + + # Run only one rule + opentaint test rule run ./rule-tests/sinks/model --rule-id `, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { runTestProject(args[0], testProjectOptions{ label: "Rule tests", + passedLine: "All rule tests passed.", tempDir: "opentaint-test-rules-*", rulesets: testRulesRuleset, outputDir: testRulesOutputDir, @@ -48,6 +63,7 @@ compiled project model. type testProjectOptions struct { label string + passedLine string // success status line; matches the documented exit-code 0 row tempDir string rulesets []string outputDir string @@ -104,7 +120,7 @@ func runTestProject(projectModelArg string, opts testProjectOptions) { if opts.includeBuiltinRules { rulesPath, err := utils.EnsureRulesPath(out) if err != nil { - out.Fatalf("Failed to prepare built-in rules: %s", err) + failf("Failed to prepare built-in rules: %s", err) } builder.AddRuleSet(rulesPath) } @@ -124,7 +140,7 @@ func runTestProject(projectModelArg string, opts testProjectOptions) { analyzerJarPath, err := ensureAnalyzerAvailable() if err != nil { - out.Fatalf("Failed to resolve analyzer: %s", err) + failf("Failed to resolve analyzer: %s", err) } builder.SetJarPath(analyzerJarPath) @@ -133,44 +149,61 @@ func runTestProject(projectModelArg string, opts testProjectOptions) { javaRunner := newAnalyzerJavaRunner() if _, err := javaRunner.EnsureJava(); err != nil { - out.Fatalf("Failed to resolve Java for analyzer: %s", err) + failf("Failed to resolve Java for analyzer: %s", err) } cmdErr, err := scanProject(builder, javaRunner) if err != nil { - out.Fatalf("%s failed: %s", opts.label, err) + failf("%s failed: %s", opts.label, err) } analyzerFail := analyzer.Classify(cmdErr) - if analyzerFail != nil { - out.Error(analyzerFail.Message) - } resultPath := filepath.Join(outputDir, "test-result.json") - fmt.Printf("Results directory: %s\n", outputDir) - fmt.Printf("Test results: %s\n", resultPath) + out.Printf("Results directory: %s", outputDir) + out.Printf("Test results: %s", resultPath) if analyzerFail != nil { + out.Error(analyzerFail.Message) + // Test runs do not activate file logging, so the log pointer is usually + // absent. For resource failures suggest the retry with more resources; + // otherwise the --debug re-run is the actionable way to see what failed. + hint := output.Suggestion{ + Description: "To stream the analyzer output, re-run with --debug:", + Command: withFlag(rerunWithoutDryRun(), "--debug"), + } + if retry, ok := retrySuggestion(analyzerFail.ExitCode, opts.timeout, opts.maxMemory); ok { + hint = retry + } + out.Suggestions(append(appendLogSuggestion(nil), hint)...) os.Exit(analyzerFail.ExitCode) } tr, err := analyzer.LoadTestResult(resultPath) if err != nil { - out.Fatalf("%s produced no readable test-result.json: %s", opts.label, err) + failf("%s produced no readable test-result.json: %s", opts.label, err) } - fmt.Printf("Passed: %d, failed: %d (false negatives: %d, false positives: %d, skipped: %d), disabled: %d\n", + out.Printf("Passed: %d, failed: %d (false negatives: %d, false positives: %d, skipped: %d), disabled: %d", len(tr.Success), tr.Failed(), len(tr.FalseNegative), len(tr.FalsePositive), len(tr.Skipped), len(tr.Disabled)) + + viewResultsCommand := utils.NewSummaryCommand(filepath.Join(outputDir, "test-results.sarif")).WithShowFindings().Build() + if tr.Failed() > 0 { out.Error(fmt.Sprintf("%s failed", opts.label)) + out.Suggestions(append(appendLogSuggestion(nil), output.Suggestion{ + Description: "To inspect the failing samples, run:", + Command: viewResultsCommand, + })...) os.Exit(2) } - fmt.Printf("%s completed successfully\n", opts.label) + out.Successf("%s", opts.passedLine) + suggest("To view the test results, run:", viewResultsCommand) } func init() { testRuleCmd.AddCommand(testRuleRunCmd) - testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleset, "ruleset", nil, "Ruleset file or directory to test (repeatable)") + testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleset, "ruleset", nil, "Ruleset to test: a YAML file or a directory of .yml or .yaml files (repeatable)") addTestRunFlags(testRuleRunCmd, &testRulesOutputDir, &testRulesTimeout, &testRulesMaxMemory, &testRulesDataflow) testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleID, "rule-id", nil, "Run only rules with this ID (repeatable)") testRuleRunCmd.Flags().StringArrayVar(&testRulesPassthrough, "passthrough-approximations", nil, "Pass-through approximation YAML file or directory (repeatable)") diff --git a/cli/cmd/update.go b/cli/cmd/update.go index 1d3a5c1bd..a1dd5ef0e 100644 --- a/cli/cmd/update.go +++ b/cli/cmd/update.go @@ -20,13 +20,19 @@ var ( var updateCmd = &cobra.Command{ Use: "update [version]", Short: "Update opentaint to the latest version", - Long: `Update opentaint to the latest version (or a specific version). + Long: `Update the opentaint binary in place to the latest release, or to the optional version argument. Only upgrades are supported; downgrading to an older version is refused. -This command detects how opentaint was installed and provides appropriate -instructions for package manager installations. For binary installations, -it performs an in-place update. +Homebrew and npm installations print the matching package-manager command instead of updating in place. Pass --check to report the latest version without downloading, or --yes to skip the confirmation prompt. -Only upgrades are supported — downgrading to an older version is refused.`, +After a successful update, remove superseded artifacts with opentaint prune.`, + Example: ` # Update to the latest release + opentaint update + + # Check for a newer version without installing + opentaint update --check + + # Update to a specific version without prompting + opentaint update 1.2.3 --yes`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { // Check installation method first @@ -35,11 +41,11 @@ Only upgrades are supported — downgrading to an older version is refused.`, switch method { case utils.InstallMethodHomebrew: out.Print("opentaint was installed via Homebrew.") - out.Print("Run: brew upgrade --cask opentaint") + suggest("To update, run:", "brew upgrade --cask opentaint") return case utils.InstallMethodNpm: out.Print("opentaint was installed via npm.") - out.Print("Run: npm install -g @seqra/opentaint@latest") + suggest("To update, run:", "npm install -g @seqra/opentaint@latest") return } @@ -75,7 +81,7 @@ Only upgrades are supported — downgrading to an older version is refused.`, out.Warnf("Could not compare versions: %s", err) out.Printf("Current: %s, Latest: %s", currentVersion, targetVersion) if !updateYes { - out.Print("Use --yes to proceed anyway.") + suggest("To proceed anyway, run:", withFlag(rerunWithoutDryRun(), "--yes")) return } } @@ -94,9 +100,8 @@ Only upgrades are supported — downgrading to an older version is refused.`, out.Section("Update Available"). Field("Current version", fmt.Sprintf("v%s", currentVersion)). Field("Latest version", fmt.Sprintf("v%s", targetVersion)). - Line(). - Text("Run 'opentaint update' to update."). Render() + suggest("To update, run:", "opentaint update") return } @@ -107,6 +112,7 @@ Only upgrades are supported — downgrading to an older version is refused.`, if !updateYes { if !out.Confirm("Proceed with update?", false) { out.Print("Update cancelled.") + suggest("To update without confirming, run:", "opentaint update --yes") return } } @@ -132,7 +138,7 @@ Only upgrades are supported — downgrading to an older version is refused.`, } out.Successf("Successfully updated to v%s", targetVersion) - suggest("To clean up old artifacts run", "opentaint prune") + suggest("To clean up old artifacts, run:", "opentaint prune") }, } diff --git a/cli/internal/analyzer/exit.go b/cli/internal/analyzer/exit.go index 6d38e7599..9e97afbda 100644 --- a/cli/internal/analyzer/exit.go +++ b/cli/internal/analyzer/exit.go @@ -37,9 +37,9 @@ func ExitMessage(code int) string { case ExitConfigError: return "project configuration error" case ExitTimeout: - return "analysis timed out — try increasing --timeout or --max-memory" + return "analysis timed out: try increasing --timeout or --max-memory" case ExitOOM: - return "out of memory — try increasing --max-memory (e.g. --max-memory 16G)" + return "out of memory: try increasing --max-memory (e.g. --max-memory 16G)" case ExitException: return "unhandled analyzer exception" default: diff --git a/docs/README.md b/docs/README.md index 760695b75..cb3fe42fd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -128,11 +128,11 @@ opentaint summary --show-findings --verbose-flow --show-code-snippets results.sa | Command | Description | |---------|-------------| -| `opentaint scan` | Analyze projects (auto-detects Maven/Gradle) | +| `opentaint scan` | Analyze projects (auto-detects the build system) | | `opentaint compile` | Build project model separately | | `opentaint project` | Create model from precompiled JARs | | `opentaint summary` | View SARIF results | -| `opentaint health` | Show resolved analyzer, autobuilder, rules, and runtime paths | +| `opentaint health` | Show dependency paths and report missing components | | `opentaint test rule` | Scaffold, test, and debug detection rules | | `opentaint test approximation` | Scaffold and test dataflow approximations | | `opentaint pull` | Download dependencies | diff --git a/docs/installation.md b/docs/installation.md index c1e400cc8..67ecb6ac8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,6 +1,6 @@ # Installation -**Prerequisites:** Same build requirements as your Java/Kotlin project (Maven or Gradle, project dependencies). Java runtime is bundled with release archives. +**Prerequisites:** Same build requirements as your project (Maven or Gradle for Java/Kotlin, the Go toolchain for Go modules). Java runtime is bundled with release archives. ## Homebrew (Linux/macOS) diff --git a/docs/usage.md b/docs/usage.md index 84f9359ed..6f777013e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -78,20 +78,20 @@ Use [CodeChecker](https://github.com/Ericsson/codechecker) for advanced result m | Command | Description | |---------|-------------| -| `opentaint scan` | Analyze projects (auto-detects Maven/Gradle, builds, and scans) | +| `opentaint scan` | Analyze projects (auto-detects the build system, builds, and scans) | | `opentaint compile` | Build project model separately from scanning | | `opentaint project` | Create project model from precompiled JARs/classes | | `opentaint summary` | View SARIF analysis results | -| `opentaint health` | Show resolved paths for the analyzer, autobuilder, rules, and Java runtime | +| `opentaint health` | Show dependency paths and report missing components | | `opentaint test rule` | Create, run, and debug detection-rule tests | | `opentaint test approximation` | Create and run dataflow-approximation tests | -| `opentaint pull` | Download analyzer dependencies | +| `opentaint pull` | Download the analysis toolchain and Java runtime | | `opentaint update` | Update to latest version | | `opentaint prune` | Remove stale downloaded artifacts and cached models | ### opentaint scan -Automatically detects Maven/Gradle projects, builds them, and performs security analysis. The source path defaults to the current directory when omitted. +Automatically detects the project's build system (Maven, Gradle, or go.mod), builds the project, and runs taint analysis over the result. The source path defaults to the current directory when omitted. On the first run, the compiled project model is cached in `~/.opentaint/cache/`. Subsequent scans of the same project reuse the cached model, skipping compilation entirely. @@ -100,10 +100,10 @@ On the first run, the compiled project model is cached in `~/.opentaint/cache/`. | `--output`, `-o` | Path to the SARIF report (default: `/sources/opentaint.sarif`) | | `--recompile` | Force recompilation even if a cached project model exists | | `--project-model` | Path to a pre-compiled project model (skips compilation) | -| `--timeout`, `-t` | Timeout for analysis (default: `15m`) | -| `--max-memory` | Maximum memory for the analyzer (default: `8G`) | -| `--severity` | Severity levels to report (default: `warning`, `error`) | -| `--ruleset` | YAML rules file or directory (default: `builtin`) | +| `--timeout`, `-t` | Maximum wall-clock time for analysis (default: `15m`) | +| `--max-memory` | Maximum analyzer heap size (default: `8G`) | +| `--severity` | Run only rules at these severity levels: `note`, `warning`, `error` (default: `warning`, `error`) | +| `--ruleset` | Rules to run: a YAML file, a directory of rules files, or `builtin` (default: `builtin`) | | `--dry-run` | Validate inputs and show what would run without compiling or scanning | | `--log-file` | Path to the log file (default: `/logs/.log`) | @@ -115,7 +115,7 @@ These flags are to work with custom approximations: |------|-------------| | `--track-external-methods` | Write external-method coverage files next to the SARIF report | | `--passthrough-approximations` | Apply pass-through approximation YAML files or directories (repeatable) | -| `--dataflow-approximations` | Apply dataflow approximation classes or Java source directories (repeatable) | +| `--dataflow-approximations` | Apply dataflow approximation classes or Java source directories (Java analysis only, repeatable) | Use external-method tracking when a scan may miss flows through library methods. The dropped-methods file shows where taint was killed because no model was available; the approximated-methods file shows methods already covered by built-in or custom models. @@ -129,7 +129,7 @@ opentaint health --rules opentaint health --analyzer ``` -With no flags, `health` shows the autobuilder, analyzer, built-in rules, and Java runtime. With a single component flag, it prints only the bare path, which is useful for scripts. +With no flags, `health` shows the autobuilder, analyzer, built-in rules, and Java runtime, and reports whether each is present. With a single component flag, it prints only the bare path, which is useful for scripts. The command exits non-zero when a selected component is missing; fetch missing components with `opentaint pull`. | Flag | Description | |------|-------------| @@ -169,13 +169,13 @@ opentaint test approximation run .opentaint/test-compiled/my-approximation \ | Command | Description | |---------|-------------| | `opentaint test approximation init ` | Create a test project with a fixed `Taint.source()` to `Taint.sink(...)` harness | -| `opentaint test approximation run ` | Run dataflow approximation tests on a compiled project model | +| `opentaint test approximation run ` | Run dataflow-approximation tests on a compiled project model | Rule and approximation test runs write `test-result.json` and `test-results.sarif` to the selected output directory. ### opentaint compile -Compiles Java and Kotlin projects and generates project models for analysis. Useful when you want to separate compilation from scanning or need to inspect the project model. +Compiles Java, Kotlin, and Go projects and generates project models for analysis. Useful when you want to separate compilation from scanning or need to inspect the project model. ```bash opentaint compile --output ./my-project-model /path/to/project @@ -184,7 +184,7 @@ opentaint scan --project-model ./my-project-model | Flag | Description | |------|-------------| -| `--output`, `-o` | Path to the result project model (required) | +| `--output`, `-o` | Path to the project model directory to create (required, must not exist) | | `--dry-run` | Validate inputs and show what would run without compiling | | `--log-file` | Path to the log file (default: `/logs/.log`) | @@ -197,7 +197,7 @@ reflects the full set the tool ran. | Flag | Description | |------|-------------| -| `--show-findings` | Show all findings | +| `--show-findings` | Show every finding in the SARIF report | | `--show-code-snippets` | Show code snippets for each finding | | `--verbose-flow` | Show full code flow steps for each finding | | `--path` | Show only findings whose file path matches this glob (`**` supported, repeatable) | @@ -224,12 +224,12 @@ opentaint scan --project-model ./project-model | Flag | Description | |------|-------------| -| `--output`, `-o` | Output directory for project.yaml (required) | -| `--source-root` | Source root directory (required) | -| `--classpath` | Classpath entries — classes or JAR files (required) | -| `--package` | Project packages (required) | -| `--dependency` | Project dependencies — JAR files | -| `--dry-run` | Validate inputs and show what would run without generating project model | +| `--output`, `-o` | Directory to write the generated project model (required, must not exist) | +| `--source-root` | Path to the project source root (required) | +| `--classpath` | Classpath entries: compiled classes directories or JAR files (required, repeatable) | +| `--package` | Packages to include in the generated model (required, repeatable) | +| `--dependency` | Additional dependency JAR files on the compile classpath (repeatable) | +| `--dry-run` | Validate inputs and show what would run without generating the project model | | `--log-file` | Path to the log file (default: `/logs/.log`) | ## Model Caching From d69761f385d8618b9bf8775ab3233762776d0af6 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sun, 23 Aug 2026 23:36:13 +0200 Subject: [PATCH 2/7] refactor(cli): Rename the model flags to --passthrough-models and --java-models --- cli/cmd/analyzer_inputs.go | 4 ++-- cli/cmd/compile_approximations.go | 2 +- cli/cmd/scan.go | 10 +++++++--- cli/cmd/test.go | 6 ++++-- cli/cmd/test_approximation_run.go | 6 +++--- cli/cmd/test_init.go | 4 ++-- cli/cmd/test_rule_run.go | 6 ++++-- cli/internal/utils/opentaint_command_builder.go | 4 ++-- docs/usage.md | 6 +++--- .../references/java.md.j2 | 2 +- .../create-rule/references/debugging.md.j2 | 2 +- skills-templates/create-rule/sections/workflow.md | 2 +- skills-templates/debug-rule/sections/workflow.md | 4 ++-- skills-templates/run-scan/sections/workflow.md | 4 ++-- skills-templates/shared/debugging.md | 2 +- .../references/debugging.md | 2 +- .../create-dataflow-approximation/references/java.md | 2 +- skills/create-rule/SKILL.md | 2 +- skills/create-rule/references/debugging.md | 4 ++-- skills/debug-rule/SKILL.md | 4 ++-- skills/run-scan/SKILL.md | 4 ++-- 21 files changed, 45 insertions(+), 37 deletions(-) diff --git a/cli/cmd/analyzer_inputs.go b/cli/cmd/analyzer_inputs.go index 3290a995f..3ed3a4571 100644 --- a/cli/cmd/analyzer_inputs.go +++ b/cli/cmd/analyzer_inputs.go @@ -6,7 +6,7 @@ import ( func addDataflowApproximations(b *AnalyzerBuilder, paths []string, analyzerJarPath, projectModelDir string) { for _, approxPath := range paths { - absApproxPath := log.AbsPathOrExit(approxPath, "dataflow-approximations") + absApproxPath := log.AbsPathOrExit(approxPath, "java-models") compiledPath, err := compileApproximationsIfNeeded(absApproxPath, analyzerJarPath, projectModelDir) if err != nil { out.Fatalf("Approximation compilation failed: %s", err) @@ -17,6 +17,6 @@ func addDataflowApproximations(b *AnalyzerBuilder, paths []string, analyzerJarPa func addPassthroughApproximations(b *AnalyzerBuilder, paths []string) { for _, passthrough := range paths { - b.AddPassthroughApproximations(log.AbsPathOrExit(passthrough, "passthrough-approximations")) + b.AddPassthroughApproximations(log.AbsPathOrExit(passthrough, "passthrough-models")) } } diff --git a/cli/cmd/compile_approximations.go b/cli/cmd/compile_approximations.go index ed61db7b1..651d5e3c1 100644 --- a/cli/cmd/compile_approximations.go +++ b/cli/cmd/compile_approximations.go @@ -18,7 +18,7 @@ import ( // bundles approximation support sources (OpentaintNdUtil, ArgumentTypeContext). const approxClassesJarPrefix = "opentaint-dataflow-approximations/" -// compileApproximationsIfNeeded checks whether a --dataflow-approximations directory +// compileApproximationsIfNeeded checks whether a --java-models directory // contains .java source files. If so, it compiles them using javac (with the // analyzer JAR + project dependencies on the classpath) and returns the path to // the compiled .class output directory. If the directory already contains only diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 8c88b4374..4b6a99641 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -158,9 +158,13 @@ func addScanFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&scanFlags.ProjectModelPath, "project-model", "", "Path to a pre-compiled project model (skips compilation)") cmd.Flags().StringVar(&scanFlags.LogFile, "log-file", "", "Path to the log file (default: /logs/.log)") - cmd.Flags().StringArrayVar(&scanFlags.PassthroughApproximations, "passthrough-approximations", nil, "Pass-through approximation YAML file or directory (repeatable)") + cmd.Flags().StringArrayVar(&scanFlags.PassthroughApproximations, "passthrough-models", nil, "Pass-through models: a YAML file or a directory of them (repeatable)") + cmd.Flags().StringArrayVar(&scanFlags.PassthroughApproximations, "passthrough-approximations", nil, "Pass-through models: a YAML file or a directory of them (repeatable)") + _ = cmd.Flags().MarkDeprecated("passthrough-approximations", "use --passthrough-models") - cmd.Flags().StringArrayVar(&scanFlags.DataflowApproximations, "dataflow-approximations", nil, "Dataflow approximation class directory or Java source directory (Java analysis only, repeatable)") + cmd.Flags().StringArrayVar(&scanFlags.DataflowApproximations, "java-models", nil, "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") + cmd.Flags().StringArrayVar(&scanFlags.DataflowApproximations, "dataflow-approximations", nil, "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") + _ = cmd.Flags().MarkDeprecated("dataflow-approximations", "use --java-models") cmd.Flags().BoolVar(&scanFlags.TrackExternalMethods, "track-external-methods", false, "Write external-method coverage files next to the SARIF report") } @@ -418,7 +422,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { } nativeBuilder.SetJarPath(analyzerJarPath) - // Process --dataflow-approximations: auto-compile .java sources if needed + // Process --java-models: auto-compile .java sources if needed addDataflowApproximations(nativeBuilder, cfg.DataflowApproximations, analyzerJarPath, absProjectModelPath) analyzerJavaRunner := newAnalyzerJavaRunner() diff --git a/cli/cmd/test.go b/cli/cmd/test.go index 8c815e022..0ff6670ee 100644 --- a/cli/cmd/test.go +++ b/cli/cmd/test.go @@ -27,7 +27,7 @@ var testApproximationCmd = &cobra.Command{ Short: "Create and run dataflow-approximation tests", Long: `Create and run dataflow-approximation tests. Approximation tests check that a dataflow approximation carries taint from source to sink across your samples. -Scaffold a test project with test approximation init, compile it with opentaint compile, then run the samples with test approximation run, supplying the approximation under test with --dataflow-approximations.`, +Scaffold a test project with test approximation init, compile it with opentaint compile, then run the samples with test approximation run, supplying the approximation under test with --java-models.`, } func init() { @@ -40,5 +40,7 @@ func addTestRunFlags(cmd *cobra.Command, outputDir *string, timeout *time.Durati cmd.Flags().StringVarP(outputDir, "output", "o", "", "Directory for test-result.json and test-results.sarif") cmd.Flags().DurationVar(timeout, "timeout", 600*time.Second, "Maximum wall-clock time for analysis (e.g. 30m, 1h)") cmd.Flags().StringVar(maxMemory, "max-memory", "8G", "Maximum analyzer heap size (e.g. 8G, 1024m)") - cmd.Flags().StringArrayVar(dataflow, "dataflow-approximations", nil, "Dataflow approximation class directory or Java source directory (Java analysis only, repeatable)") + cmd.Flags().StringArrayVar(dataflow, "java-models", nil, "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") + cmd.Flags().StringArrayVar(dataflow, "dataflow-approximations", nil, "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") + _ = cmd.Flags().MarkDeprecated("dataflow-approximations", "use --java-models") } diff --git a/cli/cmd/test_approximation_run.go b/cli/cmd/test_approximation_run.go index ca0210e9f..a8ca1d7df 100644 --- a/cli/cmd/test_approximation_run.go +++ b/cli/cmd/test_approximation_run.go @@ -20,7 +20,7 @@ var testApproximationRunCmd = &cobra.Command{ Short: "Run dataflow-approximation tests on a compiled project model", Long: `Run the samples declared in rule-test.yaml with the supplied dataflow approximations applied and report which passed. A fixed source-to-sink harness rule is applied automatically; positive samples reference it by id approximation-rule. -The project-model argument is a compiled project model directory, produced by opentaint compile. Supply the approximation under test with --dataflow-approximations. +The project-model argument is a compiled project model directory, produced by opentaint compile. Supply the approximation under test with --java-models. Results are written as test-result.json and a test-results.sarif report to --output, or to a temporary directory when unset. @@ -28,10 +28,10 @@ Compile the test project with opentaint compile before running. Inspect the resu ` + testExitCodesHelp("All approximation tests passed"), Example: ` # Run an approximation test against a compiled model - opentaint test approximation run ./approx-test/model --dataflow-approximations ./approx + opentaint test approximation run ./approx-test/model --java-models ./approx # Write results to a directory - opentaint test approximation run ./approx-test/model --dataflow-approximations ./approx -o ./results`, + opentaint test approximation run ./approx-test/model --java-models ./approx -o ./results`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { ruleDir, err := os.MkdirTemp("", "opentaint-approx-rule-*") diff --git a/cli/cmd/test_init.go b/cli/cmd/test_init.go index a14105bc6..22ab6b66e 100644 --- a/cli/cmd/test_init.go +++ b/cli/cmd/test_init.go @@ -70,7 +70,7 @@ var testApproximationInitCmd = &cobra.Command{ Short: "Create a dataflow-approximation test project", Long: `Create a Gradle test project for dataflow-approximation tests. The project pins a fixed source-to-sink rule that the samples are checked against. -The output-dir argument is the directory the project is created in. Use --dependency to add compile-only Maven dependencies for the samples. The approximation under test is not baked in; supply it at run time with --dataflow-approximations. +The output-dir argument is the directory the project is created in. Use --dependency to add compile-only Maven dependencies for the samples. The approximation under test is not baked in; supply it at run time with --java-models. The project ships a rule-test.yaml where you declare the positive and negative samples, plus a Taint.java source and sink and the fixed approximation-rule.yaml. @@ -94,7 +94,7 @@ After editing rule-test.yaml, compile the project with opentaint compile and run out.Suggestions( output.Suggestion{Description: "To add your test samples, edit:", Command: filepath.Join(dir, "rule-test.yaml")}, output.Suggestion{Description: "To compile the test project, run:", Command: fmt.Sprintf("opentaint compile %s -o %s", dir, modelDir)}, - output.Suggestion{Description: "To run the tests, run:", Command: fmt.Sprintf("opentaint test approximation run %s --dataflow-approximations ", modelDir)}, + output.Suggestion{Description: "To run the tests, run:", Command: fmt.Sprintf("opentaint test approximation run %s --java-models ", modelDir)}, ) }, } diff --git a/cli/cmd/test_rule_run.go b/cli/cmd/test_rule_run.go index 83dbc3b6e..ef6b8484a 100644 --- a/cli/cmd/test_rule_run.go +++ b/cli/cmd/test_rule_run.go @@ -28,7 +28,7 @@ var testRuleRunCmd = &cobra.Command{ Short: "Run detection-rule tests on a compiled project model", Long: `Run detection rules against the samples declared in rule-test.yaml and report which passed. The built-in rules are always included. -The project-model argument is a compiled project model directory, produced by opentaint compile. Add your own rules with --ruleset, narrow the run to specific rules with --rule-id, and apply approximations with --dataflow-approximations or --passthrough-approximations. +The project-model argument is a compiled project model directory, produced by opentaint compile. Add your own rules with --ruleset, narrow the run to specific rules with --rule-id, and apply models with --java-models or --passthrough-models. Results are written as test-result.json and a test-results.sarif report to --output, or to a temporary directory when unset. @@ -206,5 +206,7 @@ func init() { testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleset, "ruleset", nil, "Ruleset to test: a YAML file or a directory of .yml or .yaml files (repeatable)") addTestRunFlags(testRuleRunCmd, &testRulesOutputDir, &testRulesTimeout, &testRulesMaxMemory, &testRulesDataflow) testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleID, "rule-id", nil, "Run only rules with this ID (repeatable)") - testRuleRunCmd.Flags().StringArrayVar(&testRulesPassthrough, "passthrough-approximations", nil, "Pass-through approximation YAML file or directory (repeatable)") + testRuleRunCmd.Flags().StringArrayVar(&testRulesPassthrough, "passthrough-models", nil, "Pass-through models: a YAML file or a directory of them (repeatable)") + testRuleRunCmd.Flags().StringArrayVar(&testRulesPassthrough, "passthrough-approximations", nil, "Pass-through models: a YAML file or a directory of them (repeatable)") + _ = testRuleRunCmd.Flags().MarkDeprecated("passthrough-approximations", "use --passthrough-models") } diff --git a/cli/internal/utils/opentaint_command_builder.go b/cli/internal/utils/opentaint_command_builder.go index 1356a557b..369f62cd5 100644 --- a/cli/internal/utils/opentaint_command_builder.go +++ b/cli/internal/utils/opentaint_command_builder.go @@ -184,7 +184,7 @@ func (cb *OpentaintCommandBuilder) WithRuleID(ruleIDs []string) *OpentaintComman func (cb *OpentaintCommandBuilder) WithPassthroughApproximations(paths []string) *OpentaintCommandBuilder { for _, p := range paths { if p != "" { - cb.arrayFlags["passthrough-approximations"] = append(cb.arrayFlags["passthrough-approximations"], p) + cb.arrayFlags["passthrough-models"] = append(cb.arrayFlags["passthrough-models"], p) } } return cb @@ -193,7 +193,7 @@ func (cb *OpentaintCommandBuilder) WithPassthroughApproximations(paths []string) func (cb *OpentaintCommandBuilder) WithDataflowApproximations(paths []string) *OpentaintCommandBuilder { for _, p := range paths { if p != "" { - cb.arrayFlags["dataflow-approximations"] = append(cb.arrayFlags["dataflow-approximations"], p) + cb.arrayFlags["java-models"] = append(cb.arrayFlags["java-models"], p) } } return cb diff --git a/docs/usage.md b/docs/usage.md index 6f777013e..fcaf3ad2f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -114,8 +114,8 @@ These flags are to work with custom approximations: | Flag | Description | |------|-------------| | `--track-external-methods` | Write external-method coverage files next to the SARIF report | -| `--passthrough-approximations` | Apply pass-through approximation YAML files or directories (repeatable) | -| `--dataflow-approximations` | Apply dataflow approximation classes or Java source directories (Java analysis only, repeatable) | +| `--passthrough-models` | Apply pass-through model YAML files or directories (repeatable) | +| `--java-models` | Apply Java dataflow model classes or source directories (repeatable) | Use external-method tracking when a scan may miss flows through library methods. The dropped-methods file shows where taint was killed because no model was available; the approximated-methods file shows methods already covered by built-in or custom models. @@ -163,7 +163,7 @@ opentaint test rule reachability java/security/my-rule.yaml:my-rule --project-mo opentaint test approximation init .opentaint/test-projects/my-approximation opentaint compile .opentaint/test-projects/my-approximation -o .opentaint/test-compiled/my-approximation opentaint test approximation run .opentaint/test-compiled/my-approximation \ - --dataflow-approximations .opentaint/dataflow/my-approximation + --java-models .opentaint/dataflow/my-approximation ``` | Command | Description | diff --git a/skills-templates/create-dataflow-approximation/references/java.md.j2 b/skills-templates/create-dataflow-approximation/references/java.md.j2 index 523eb6e5b..943916079 100644 --- a/skills-templates/create-dataflow-approximation/references/java.md.j2 +++ b/skills-templates/create-dataflow-approximation/references/java.md.j2 @@ -65,7 +65,7 @@ Run `test approximation run` over the compiled test project applying this batch' ```bash opentaint test approximation run .opentaint/test-compiled/ \ -o .opentaint/test-results/ \ - --dataflow-approximations .opentaint/dataflow/ + --java-models .opentaint/dataflow/ ``` `test approximation run` applies its own bundled fixed source→sink rule automatically — you don't author or pass one. The CLI auto-compiles the `.java` sources against the analyzer JAR (for `@Approximate`, `OpentaintNdUtil`, `ArgumentTypeContext`) and the project's dependencies; if compilation fails it reports the errors and aborts before the tests. A positive sample is a `falseNegative` until the model propagates taint. Read the result with the bundled script — it prints the pass/fail counts and names each failing sample, so you don't parse the JSON by hand: diff --git a/skills-templates/create-rule/references/debugging.md.j2 b/skills-templates/create-rule/references/debugging.md.j2 index dd8fce4a0..bd2774264 100644 --- a/skills-templates/create-rule/references/debugging.md.j2 +++ b/skills-templates/create-rule/references/debugging.md.j2 @@ -8,7 +8,7 @@ When a positive won't pass and the suspicion is a library method on its flow dro opentaint scan --project-model .opentaint/test-compiled// \ -o .opentaint/test-results///diag.sarif \ --ruleset builtin --ruleset .opentaint/rules --ruleset .opentaint/test-projects///test-rules \ - --passthrough-approximations .opentaint/pass-through \ + --passthrough-models .opentaint/pass-through \ --track-external-methods ``` diff --git a/skills-templates/create-rule/sections/workflow.md b/skills-templates/create-rule/sections/workflow.md index 61858ed2e..8269c9db1 100644 --- a/skills-templates/create-rule/sections/workflow.md +++ b/skills-templates/create-rule/sections/workflow.md @@ -24,7 +24,7 @@ Run the rule tests directly as a foreground, blocking command and wait for exit opentaint test rule run .opentaint/test-compiled// \ -o .opentaint/test-results// \ --ruleset .opentaint/rules --ruleset .opentaint/test-projects///test-rules \ - --passthrough-approximations .opentaint/pass-through + --passthrough-models .opentaint/pass-through ``` `test rule run` auto-loads the built-in rules, so pass only your custom rulesets. Apply the passthrough approximations as-is, an empty one is harmless. Read the result with the bundled script — it prints the pass/fail counts and names the failing samples, so you never parse the JSON by hand: diff --git a/skills-templates/debug-rule/sections/workflow.md b/skills-templates/debug-rule/sections/workflow.md index 3a490974f..888b296d2 100644 --- a/skills-templates/debug-rule/sections/workflow.md +++ b/skills-templates/debug-rule/sections/workflow.md @@ -7,8 +7,8 @@ opentaint test rule reachability \ --project-model \ -o /report.sarif \ --ruleset builtin --ruleset .opentaint/rules \ - --passthrough-approximations .opentaint/pass-through \ - --dataflow-approximations .opentaint/dataflow + --passthrough-models .opentaint/pass-through \ + --java-models .opentaint/dataflow ``` `` is `.opentaint/test-results/` for a test model, `.opentaint/results` for the main scan. The per-instruction facts are in the sibling `/debug-ifds-fact-reachability.sarif`, not the `-o` file — the `-o` SARIF only shows whether the rule fired. Read that sibling to find the kill: diff --git a/skills-templates/run-scan/sections/workflow.md b/skills-templates/run-scan/sections/workflow.md index aeab157e3..6ddb900c2 100644 --- a/skills-templates/run-scan/sections/workflow.md +++ b/skills-templates/run-scan/sections/workflow.md @@ -10,8 +10,8 @@ opentaint scan --project-model .opentaint/project \ ``` - `--rule-id ` — restrict to specific rules (repeatable, one per input rule ID); every unnamed rule is dropped, including library `refs`, so list every id the restricted rules depend on. Omit to run all loaded rules -- `--passthrough-approximations .opentaint/pass-through` — add when that directory exists: passThrough configs override built-ins at the rule level, a provided rule overriding a built-in only when it matches one -- `--dataflow-approximations .opentaint/dataflow` — add when that directory exists: code-based approximations (sources auto-compiled; pre-compiled `.class` dirs passed through as-is) +- `--passthrough-models .opentaint/pass-through` — add when that directory exists: passThrough configs override built-ins at the rule level, a provided rule overriding a built-in only when it matches one +- `--java-models .opentaint/dataflow` — add when that directory exists: code-based approximations (sources auto-compiled; pre-compiled `.class` dirs passed through as-is) Both approximation-dir flags walk their trees recursively; pass each parent directory once, not every package or batch separately. diff --git a/skills-templates/shared/debugging.md b/skills-templates/shared/debugging.md index 8b7ce3171..8d80bd6ce 100644 --- a/skills-templates/shared/debugging.md +++ b/skills-templates/shared/debugging.md @@ -15,7 +15,7 @@ opentaint test rule reachability \ - `` — the one rule whose sample routes taint through the code under test (`.yaml:`). One rule per run — across many rules the trace is unusably huge. Its library `refs` are collected automatically - read the sibling `/debug-ifds-fact-reachability.sarif`, not the `-o` file. The `-o` SARIF only shows default scan output. The sibling holds the per-instruction facts that show where taint dies -- apply the approximations the failing run used so the trace matches it: `--passthrough-approximations ` and/or `--dataflow-approximations `. Taint dying at an approximated call then means that approximation isn't propagating +- apply the approximations the failing run used so the trace matches it: `--passthrough-models ` and/or `--java-models `. Taint dying at an approximated call then means that approximation isn't propagating - debug the exact run that showed the problem — same model, rulesets, approximation dirs — or you're debugging something else ## Reading the trace diff --git a/skills/create-dataflow-approximation/references/debugging.md b/skills/create-dataflow-approximation/references/debugging.md index 8b7ce3171..8d80bd6ce 100644 --- a/skills/create-dataflow-approximation/references/debugging.md +++ b/skills/create-dataflow-approximation/references/debugging.md @@ -15,7 +15,7 @@ opentaint test rule reachability \ - `` — the one rule whose sample routes taint through the code under test (`.yaml:`). One rule per run — across many rules the trace is unusably huge. Its library `refs` are collected automatically - read the sibling `/debug-ifds-fact-reachability.sarif`, not the `-o` file. The `-o` SARIF only shows default scan output. The sibling holds the per-instruction facts that show where taint dies -- apply the approximations the failing run used so the trace matches it: `--passthrough-approximations ` and/or `--dataflow-approximations `. Taint dying at an approximated call then means that approximation isn't propagating +- apply the approximations the failing run used so the trace matches it: `--passthrough-models ` and/or `--java-models `. Taint dying at an approximated call then means that approximation isn't propagating - debug the exact run that showed the problem — same model, rulesets, approximation dirs — or you're debugging something else ## Reading the trace diff --git a/skills/create-dataflow-approximation/references/java.md b/skills/create-dataflow-approximation/references/java.md index 523eb6e5b..943916079 100644 --- a/skills/create-dataflow-approximation/references/java.md +++ b/skills/create-dataflow-approximation/references/java.md @@ -65,7 +65,7 @@ Run `test approximation run` over the compiled test project applying this batch' ```bash opentaint test approximation run .opentaint/test-compiled/ \ -o .opentaint/test-results/ \ - --dataflow-approximations .opentaint/dataflow/ + --java-models .opentaint/dataflow/ ``` `test approximation run` applies its own bundled fixed source→sink rule automatically — you don't author or pass one. The CLI auto-compiles the `.java` sources against the analyzer JAR (for `@Approximate`, `OpentaintNdUtil`, `ArgumentTypeContext`) and the project's dependencies; if compilation fails it reports the errors and aborts before the tests. A positive sample is a `falseNegative` until the model propagates taint. Read the result with the bundled script — it prints the pass/fail counts and names each failing sample, so you don't parse the JSON by hand: diff --git a/skills/create-rule/SKILL.md b/skills/create-rule/SKILL.md index 323c89356..d3dfe112d 100644 --- a/skills/create-rule/SKILL.md +++ b/skills/create-rule/SKILL.md @@ -49,7 +49,7 @@ Run the rule tests directly as a foreground, blocking command and wait for exit opentaint test rule run .opentaint/test-compiled// \ -o .opentaint/test-results// \ --ruleset .opentaint/rules --ruleset .opentaint/test-projects///test-rules \ - --passthrough-approximations .opentaint/pass-through + --passthrough-models .opentaint/pass-through ``` `test rule run` auto-loads the built-in rules, so pass only your custom rulesets. Apply the passthrough approximations as-is, an empty one is harmless. Read the result with the bundled script — it prints the pass/fail counts and names the failing samples, so you never parse the JSON by hand: diff --git a/skills/create-rule/references/debugging.md b/skills/create-rule/references/debugging.md index e6d8f8ad1..b280d46ea 100644 --- a/skills/create-rule/references/debugging.md +++ b/skills/create-rule/references/debugging.md @@ -15,7 +15,7 @@ opentaint test rule reachability \ - `` — the one rule whose sample routes taint through the code under test (`.yaml:`). One rule per run — across many rules the trace is unusably huge. Its library `refs` are collected automatically - read the sibling `/debug-ifds-fact-reachability.sarif`, not the `-o` file. The `-o` SARIF only shows default scan output. The sibling holds the per-instruction facts that show where taint dies -- apply the approximations the failing run used so the trace matches it: `--passthrough-approximations ` and/or `--dataflow-approximations `. Taint dying at an approximated call then means that approximation isn't propagating +- apply the approximations the failing run used so the trace matches it: `--passthrough-models ` and/or `--java-models `. Taint dying at an approximated call then means that approximation isn't propagating - debug the exact run that showed the problem — same model, rulesets, approximation dirs — or you're debugging something else ## Reading the trace @@ -35,7 +35,7 @@ When a positive won't pass and the suspicion is a library method on its flow dro opentaint scan --project-model .opentaint/test-compiled// \ -o .opentaint/test-results///diag.sarif \ --ruleset builtin --ruleset .opentaint/rules --ruleset .opentaint/test-projects///test-rules \ - --passthrough-approximations .opentaint/pass-through \ + --passthrough-models .opentaint/pass-through \ --track-external-methods ``` diff --git a/skills/debug-rule/SKILL.md b/skills/debug-rule/SKILL.md index 0907d496d..d5a1d464b 100644 --- a/skills/debug-rule/SKILL.md +++ b/skills/debug-rule/SKILL.md @@ -30,8 +30,8 @@ opentaint test rule reachability \ --project-model \ -o /report.sarif \ --ruleset builtin --ruleset .opentaint/rules \ - --passthrough-approximations .opentaint/pass-through \ - --dataflow-approximations .opentaint/dataflow + --passthrough-models .opentaint/pass-through \ + --java-models .opentaint/dataflow ``` `` is `.opentaint/test-results/` for a test model, `.opentaint/results` for the main scan. The per-instruction facts are in the sibling `/debug-ifds-fact-reachability.sarif`, not the `-o` file — the `-o` SARIF only shows whether the rule fired. Read that sibling to find the kill: diff --git a/skills/run-scan/SKILL.md b/skills/run-scan/SKILL.md index 8c363c092..c81bad9ad 100644 --- a/skills/run-scan/SKILL.md +++ b/skills/run-scan/SKILL.md @@ -33,8 +33,8 @@ opentaint scan --project-model .opentaint/project \ ``` - `--rule-id ` — restrict to specific rules (repeatable, one per input rule ID); every unnamed rule is dropped, including library `refs`, so list every id the restricted rules depend on. Omit to run all loaded rules -- `--passthrough-approximations .opentaint/pass-through` — add when that directory exists: passThrough configs override built-ins at the rule level, a provided rule overriding a built-in only when it matches one -- `--dataflow-approximations .opentaint/dataflow` — add when that directory exists: code-based approximations (sources auto-compiled; pre-compiled `.class` dirs passed through as-is) +- `--passthrough-models .opentaint/pass-through` — add when that directory exists: passThrough configs override built-ins at the rule level, a provided rule overriding a built-in only when it matches one +- `--java-models .opentaint/dataflow` — add when that directory exists: code-based approximations (sources auto-compiled; pre-compiled `.class` dirs passed through as-is) Both approximation-dir flags walk their trees recursively; pass each parent directory once, not every package or batch separately. From d2e674f6d545c51d3bbea63715f555099f58f4b1 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 24 Aug 2026 01:01:08 +0200 Subject: [PATCH 3/7] style(cli): drop semicolons from help text, messages, and comments House style: no semicolons in prose. Semicolon-joined sentences in the Long descriptions, flag help, the dry-run status line, and comments are split into separate sentences, and '; defaults to X' flag suffixes become '(defaults to X)'. The mirrored line in docs/usage.md is updated to match. --- cli/cmd/compile.go | 2 +- cli/cmd/dry_run.go | 4 ++-- cli/cmd/health.go | 2 +- cli/cmd/project.go | 2 +- cli/cmd/prune.go | 2 +- cli/cmd/scan.go | 4 ++-- cli/cmd/summary.go | 6 +++--- cli/cmd/test.go | 2 +- cli/cmd/test_approximation_run.go | 2 +- cli/cmd/test_init.go | 6 +++--- cli/cmd/test_rule_reachability.go | 2 +- cli/cmd/test_rule_run.go | 6 +++--- cli/cmd/update.go | 2 +- docs/usage.md | 2 +- 14 files changed, 22 insertions(+), 22 deletions(-) diff --git a/cli/cmd/compile.go b/cli/cmd/compile.go index 965b3973f..dfec1651f 100644 --- a/cli/cmd/compile.go +++ b/cli/cmd/compile.go @@ -44,7 +44,7 @@ var compileCmd = &cobra.Command{ Args: cobra.ExactArgs(1), // require exactly one argument Long: `OpenTaint detects the build system, resolves the project's modules and dependencies, and compiles the project into a reusable model. -The project argument is the path to the project root and is required. Pass --output to name the project model directory to create; it must not already exist. +The project argument is the path to the project root and is required. Pass --output to name the project model directory to create. It must not already exist. The project model is written to the --output directory and can be reused by later scans without rebuilding. diff --git a/cli/cmd/dry_run.go b/cli/cmd/dry_run.go index c6086cb4e..39d483a55 100644 --- a/cli/cmd/dry_run.go +++ b/cli/cmd/dry_run.go @@ -14,7 +14,7 @@ func failOnInvalidInputs(validate func() error) { // runDryRun prints the standard dry-run tail: a status line naming the skipped // action, then a suggestion to repeat the same invocation without --dry-run. func runDryRun(skippedAction string) { - out.Printf("Dry run complete. Inputs validated; %s skipped.", skippedAction) + out.Printf("Dry run complete. Inputs validated, %s skipped.", skippedAction) suggest("To run for real, run:", rerunWithoutDryRun()) } @@ -34,7 +34,7 @@ func rerunWithoutDryRun() string { } // shellQuote single-quotes an argument that would break when copy-pasted into -// a shell (spaces, quotes); plain arguments pass through unchanged. +// a shell (spaces, quotes). Plain arguments pass through unchanged. func shellQuote(arg string) string { if arg != "" && !strings.ContainsAny(arg, " \t'\"") { return arg diff --git a/cli/cmd/health.go b/cli/cmd/health.go index 4e3de53bd..df3f1b2dc 100644 --- a/cli/cmd/health.go +++ b/cli/cmd/health.go @@ -30,7 +30,7 @@ var healthCmd = &cobra.Command{ Short: "Show dependency paths and report missing components", Long: `Show the on-disk paths for the autobuilder, analyzer, built-in rules, and Java runtime, and report whether each component is present. -Select components with --autobuilder, --analyzer, --rules, or --runtime; with no flag, all four are reported. When exactly one component is selected, only its path is printed, which suits scripting. Only the built-in rules are fetched on demand; no other artifact is downloaded. +Select components with --autobuilder, --analyzer, --rules, or --runtime. With no flag, all four are reported. When exactly one component is selected, only its path is printed, which suits scripting. Only the built-in rules are fetched on demand. No other artifact is downloaded. The command exits non-zero when any selected component is missing. Download the missing components with opentaint pull.`, Example: ` # Report all components and their paths diff --git a/cli/cmd/project.go b/cli/cmd/project.go index 0e8ff5cd2..5f3e1d5d6 100644 --- a/cli/cmd/project.go +++ b/cli/cmd/project.go @@ -220,7 +220,7 @@ var ( var projectCmd = &cobra.Command{ Use: "project", Short: "Create a project model from precompiled JARs or classes", - Long: `Create a project model directly from precompiled JARs or classes, without running a build. OpenTaint inspects the supplied classpath, detects the modules and dependencies, and writes a project.yaml describing the project for later analysis. Use this when you already have compiled artifacts; opentaint compile builds a model from sources instead. + Long: `Create a project model directly from precompiled JARs or classes, without running a build. OpenTaint inspects the supplied classpath, detects the modules and dependencies, and writes a project.yaml describing the project for later analysis. Use this when you already have compiled artifacts. To build a model from sources, use opentaint compile instead. All inputs are supplied as flags. Provide --source-root for the project sources, one or more --classpath entries for the compiled classes or JARs, and one or more --package names to include. Add --dependency for extra JAR files on the compile classpath. diff --git a/cli/cmd/prune.go b/cli/cmd/prune.go index 162a3a4c2..1fc24300b 100644 --- a/cli/cmd/prune.go +++ b/cli/cmd/prune.go @@ -60,7 +60,7 @@ var pruneCmd = &cobra.Command{ Short: "Remove stale downloaded artifacts from the cache", Long: `Remove stale downloaded artifacts from the local cache (~/.opentaint): superseded analyzer and autobuilder JARs, old rules, JDK and JRE versions that no longer match the configured one, and cached project models. -Select categories with --artifacts, --rules, --jdk, --models, --logs, or --install. Without a category flag, prune removes artifacts, rules, jdk, and models; --all removes everything, including logs and install-tier artifacts, and cannot be combined with a specific category flag. +Select categories with --artifacts, --rules, --jdk, --models, --logs, or --install. Without a category flag, prune removes artifacts, rules, jdk, and models. The --all flag removes everything, including logs and install-tier artifacts, and cannot be combined with a specific category flag. Preview the deletions with --dry-run, and skip the confirmation prompt with --yes. Restore install-tier artifacts afterward with opentaint pull.`, Example: ` # Prune the default categories after confirming diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 4b6a99641..85bc267dd 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -77,7 +77,7 @@ var scanCmd = &cobra.Command{ Args: cobra.MaximumNArgs(1), Long: `Scan a project for vulnerabilities. OpenTaint detects the build system, builds the project, and runs taint analysis over the result. -The optional source-path argument is the project root and defaults to the current directory. Pass --project-model to analyze a pre-compiled project model instead of building; source-path and --project-model are mutually exclusive. +The optional source-path argument is the project root and defaults to the current directory. Pass --project-model to analyze a pre-compiled project model instead of building. The source-path argument and --project-model are mutually exclusive. Findings are written as a SARIF report to --output, or into the project model directory when unset, and summarized on completion. @@ -495,7 +495,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { if analyzerFail == nil { out.Successf("Reachability analysis completed.") } - // The reachability report is the command's deliverable; point at it, + // The reachability report is the command's deliverable. Point at it, // never at the main SARIF. reachabilityReportPath := filepath.Join(filepath.Dir(absSarifReportPath), "debug-ifds-fact-reachability.sarif") suggestions = append(suggestions, output.Suggestion{ diff --git a/cli/cmd/summary.go b/cli/cmd/summary.go index 58231fa4e..7be7d9f7d 100644 --- a/cli/cmd/summary.go +++ b/cli/cmd/summary.go @@ -14,7 +14,7 @@ var summaryCmd = &cobra.Command{ Args: cobra.ExactArgs(1), // require exactly one argument Long: `Summarize a SARIF report on the terminal. OpenTaint counts the findings by severity, groups them, and shows which rules ran and which produced results. -The required positional argument is the path to a SARIF report, such as one written by opentaint scan or opentaint test. Pass --show-findings to list every finding; narrow the listing with --severity, --rule-id, or --path, and expand code flows with --show-code-snippets and --verbose-flow. +The required positional argument is the path to a SARIF report, such as one written by opentaint scan or opentaint test. Pass --show-findings to list every finding. Narrow the listing with --severity, --rule-id, or --path, and expand code flows with --show-code-snippets and --verbose-flow. The report is read only: the summary and any findings are printed to the terminal and nothing is written to disk. @@ -88,9 +88,9 @@ func init() { summaryCmd.Flags().StringArrayVar(&summarySeverities, "severity", nil, "Show only findings at these SARIF levels: note, warning, error, none (repeatable)") summaryCmd.Flags().StringArrayVar(&summaryRuleIDs, "rule-id", nil, "Show only findings from this rule: full id, leaf name, or glob (repeatable)") summaryCmd.Flags().StringArrayVar(&summaryFingerprints, "partial-fingerprint", nil, "Show only findings whose partial fingerprint starts with this value (git-hash style, repeatable)") - summaryCmd.Flags().StringVar(&summaryFingerprintKey, "partial-fingerprint-key", "", "partialFingerprints key matched by --partial-fingerprint; defaults to vulnerabilityWithTraceHash/v1") + summaryCmd.Flags().StringVar(&summaryFingerprintKey, "partial-fingerprint-key", "", "partialFingerprints key matched by --partial-fingerprint (defaults to vulnerabilityWithTraceHash/v1)") summaryCmd.Flags().IntVar(&summaryMaxNestingLevel, "max-nesting-level", -1, "Collapse code-flow steps deeper than this call-nesting level (-1 = no cap)") - summaryCmd.Flags().StringVar(&summaryGroupBy, "group-by", "", "Group the --show-findings listing by: severity, rule-id, file-path; defaults to file-path") + summaryCmd.Flags().StringVar(&summaryGroupBy, "group-by", "", "Group the --show-findings listing by: severity, rule-id, file-path (defaults to file-path)") summaryCmd.Flags().StringVar(&summaryCodeFlow, "code-flow", "", "Render code flows: \"all\", a 1-based index, or unset (first only)") } diff --git a/cli/cmd/test.go b/cli/cmd/test.go index 0ff6670ee..8598c601b 100644 --- a/cli/cmd/test.go +++ b/cli/cmd/test.go @@ -9,7 +9,7 @@ import ( var testCmd = &cobra.Command{ Use: "test", Short: "Create and run rule and approximation tests", - Long: `Create, run, and debug rule and approximation tests. Rule tests check detection rules against annotated sample projects; approximation tests check dataflow approximations the same way. + Long: `Create, run, and debug rule and approximation tests. Rule tests check detection rules against annotated sample projects. Approximation tests check dataflow approximations the same way. Scaffold a project with init, compile it with opentaint compile, then run the samples with test rule run or test approximation run. Use test rule reachability to debug why a single rule does or does not fire.`, } diff --git a/cli/cmd/test_approximation_run.go b/cli/cmd/test_approximation_run.go index a8ca1d7df..6bb796a3a 100644 --- a/cli/cmd/test_approximation_run.go +++ b/cli/cmd/test_approximation_run.go @@ -18,7 +18,7 @@ var ( var testApproximationRunCmd = &cobra.Command{ Use: "run ", Short: "Run dataflow-approximation tests on a compiled project model", - Long: `Run the samples declared in rule-test.yaml with the supplied dataflow approximations applied and report which passed. A fixed source-to-sink harness rule is applied automatically; positive samples reference it by id approximation-rule. + Long: `Run the samples declared in rule-test.yaml with the supplied dataflow approximations applied and report which passed. A fixed source-to-sink harness rule is applied automatically. Positive samples reference it by id approximation-rule. The project-model argument is a compiled project model directory, produced by opentaint compile. Supply the approximation under test with --java-models. diff --git a/cli/cmd/test_init.go b/cli/cmd/test_init.go index 22ab6b66e..c947ad9aa 100644 --- a/cli/cmd/test_init.go +++ b/cli/cmd/test_init.go @@ -19,9 +19,9 @@ var initRuleSourcesOnly bool var testRuleInitCmd = &cobra.Command{ Use: "init ", Short: "Create rule test projects with source and sink harnesses", - Long: `Create one or two Gradle test projects for detection-rule tests. The sinks project tests sink rules against a generic taint source; the sources project tests source rules against a generic taint sink. + Long: `Create one or two Gradle test projects for detection-rule tests. The sinks project tests sink rules against a generic taint source. The sources project tests source rules against a generic taint sink. -The output-dir argument is the parent directory the projects are created under. By default both are scaffolded, as output-dir/sinks and output-dir/sources; pass --sinks-only or --sources-only to create just one. Use --dependency to add compile-only Maven dependencies for the samples. +The output-dir argument is the parent directory the projects are created under. By default both are scaffolded, as output-dir/sinks and output-dir/sources. Pass --sinks-only or --sources-only to create just one. Use --dependency to add compile-only Maven dependencies for the samples. Each project ships a rule-test.yaml where you declare the positive and negative samples, plus a Taint.java source and sink harness. @@ -70,7 +70,7 @@ var testApproximationInitCmd = &cobra.Command{ Short: "Create a dataflow-approximation test project", Long: `Create a Gradle test project for dataflow-approximation tests. The project pins a fixed source-to-sink rule that the samples are checked against. -The output-dir argument is the directory the project is created in. Use --dependency to add compile-only Maven dependencies for the samples. The approximation under test is not baked in; supply it at run time with --java-models. +The output-dir argument is the directory the project is created in. Use --dependency to add compile-only Maven dependencies for the samples. The approximation under test is not baked in. Supply it at run time with --java-models. The project ships a rule-test.yaml where you declare the positive and negative samples, plus a Taint.java source and sink and the fixed approximation-rule.yaml. diff --git a/cli/cmd/test_rule_reachability.go b/cli/cmd/test_rule_reachability.go index 58bfc6233..5d61019b2 100644 --- a/cli/cmd/test_rule_reachability.go +++ b/cli/cmd/test_rule_reachability.go @@ -11,7 +11,7 @@ var testRuleReachabilityCmd = &cobra.Command{ Short: "Trace why a rule can or cannot reach its facts", Long: `Scan a project with a single rule and write a fact-reachability SARIF report so you can see why that rule does or does not fire. Referenced library source and sink rules are collected and analyzed automatically. -The rule-id argument selects the one rule to trace. The optional source-path argument is the project root and defaults to the current directory. Pass --project-model to trace a pre-compiled model instead; source-path and --project-model are mutually exclusive. Use --entry-points to start the analysis from a specific method. +The rule-id argument selects the one rule to trace. The optional source-path argument is the project root and defaults to the current directory. Pass --project-model to trace a pre-compiled model instead. The source-path argument and --project-model are mutually exclusive. Use --entry-points to start the analysis from a specific method. The report is written as debug-ifds-fact-reachability.sarif next to the main SARIF report. diff --git a/cli/cmd/test_rule_run.go b/cli/cmd/test_rule_run.go index ef6b8484a..f6a1492a8 100644 --- a/cli/cmd/test_rule_run.go +++ b/cli/cmd/test_rule_run.go @@ -63,7 +63,7 @@ Compile the test project with opentaint compile before running. Inspect the resu type testProjectOptions struct { label string - passedLine string // success status line; matches the documented exit-code 0 row + passedLine string // success status line, matching the documented exit-code 0 row tempDir string rulesets []string outputDir string @@ -165,8 +165,8 @@ func runTestProject(projectModelArg string, opts testProjectOptions) { if analyzerFail != nil { out.Error(analyzerFail.Message) // Test runs do not activate file logging, so the log pointer is usually - // absent. For resource failures suggest the retry with more resources; - // otherwise the --debug re-run is the actionable way to see what failed. + // absent. For resource failures suggest the retry with more resources. + // Otherwise the --debug re-run is the actionable way to see what failed. hint := output.Suggestion{ Description: "To stream the analyzer output, re-run with --debug:", Command: withFlag(rerunWithoutDryRun(), "--debug"), diff --git a/cli/cmd/update.go b/cli/cmd/update.go index a1dd5ef0e..19d83a8e4 100644 --- a/cli/cmd/update.go +++ b/cli/cmd/update.go @@ -20,7 +20,7 @@ var ( var updateCmd = &cobra.Command{ Use: "update [version]", Short: "Update opentaint to the latest version", - Long: `Update the opentaint binary in place to the latest release, or to the optional version argument. Only upgrades are supported; downgrading to an older version is refused. + Long: `Update the opentaint binary in place to the latest release, or to the optional version argument. Only upgrades are supported. Downgrading to an older version is refused. Homebrew and npm installations print the matching package-manager command instead of updating in place. Pass --check to report the latest version without downloading, or --yes to skip the confirmation prompt. diff --git a/docs/usage.md b/docs/usage.md index fcaf3ad2f..ac216644f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -129,7 +129,7 @@ opentaint health --rules opentaint health --analyzer ``` -With no flags, `health` shows the autobuilder, analyzer, built-in rules, and Java runtime, and reports whether each is present. With a single component flag, it prints only the bare path, which is useful for scripts. The command exits non-zero when a selected component is missing; fetch missing components with `opentaint pull`. +With no flags, `health` shows the autobuilder, analyzer, built-in rules, and Java runtime, and reports whether each is present. With a single component flag, it prints only the bare path, which is useful for scripts. The command exits non-zero when a selected component is missing. Fetch missing components with `opentaint pull`. | Flag | Description | |------|-------------| From dbe650ad4b100aa59321ddb8d275241df8b6889a Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 24 Aug 2026 01:32:14 +0200 Subject: [PATCH 4/7] style(docs): drop the remaining semicolons in usage.md The four spots predate this branch (the external-methods paragraph, the summary section intro, and the global --color bullet). Rewritten as separate sentences to match the no-semicolon prose rule. --- docs/usage.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index ac216644f..00089ab0d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -117,7 +117,7 @@ These flags are to work with custom approximations: | `--passthrough-models` | Apply pass-through model YAML files or directories (repeatable) | | `--java-models` | Apply Java dataflow model classes or source directories (repeatable) | -Use external-method tracking when a scan may miss flows through library methods. The dropped-methods file shows where taint was killed because no model was available; the approximated-methods file shows methods already covered by built-in or custom models. +Use external-method tracking when a scan may miss flows through library methods. The dropped-methods file shows where taint was killed because no model was available. The approximated-methods file shows methods already covered by built-in or custom models. ### opentaint health @@ -190,9 +190,9 @@ opentaint scan --project-model ./my-project-model ### opentaint summary -View findings from a SARIF report. By default it prints the Scan Summary; add +View findings from a SARIF report. By default it prints the Scan Summary. Add `--show-findings` for the detailed listing. The filter flags below narrow the -whole summary (both the counts and the listing); `Rules executed` always +whole summary (both the counts and the listing). `Rules executed` always reflects the full set the tool ran. | Flag | Description | @@ -267,6 +267,6 @@ These options apply to all commands: - `--java-version int` — Java version for analyzer (default: 21) - `--quiet` / `-q` — Suppress interactive output (spinners, progress bars, JAR streaming) - `--debug` / `-d` — Enable debug output (stream JAR subprocess output, show debug fields) -- `--color string` — Color mode (`auto`, `always`, `never`); defaults to `auto` (detects terminal) +- `--color string` — Color mode (`auto`, `always`, `never`), defaults to `auto` (detects terminal) For persistent configuration using files or environment variables, see the [Configuration](configuration.md) documentation. From 311bf5e0b82b5634c1b33d4eb26858330d8007f1 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 24 Aug 2026 03:16:36 +0200 Subject: [PATCH 5/7] fix(cli): keep values from both spellings of renamed flags, quote shell metacharacters Registering a renamed flag and its deprecated alias as two stock pflag StringArrayVar flags bound to one slice made the alias's first value replace whatever the new spelling had already collected, silently dropping models from the analysis. The two spellings now share an appending value type, so mixed invocations accumulate in command-line order. Applies to --passthrough-models and --java-models on scan and the test-run commands. shellQuote quoted only whitespace and quote characters, so a suggested re-run containing a glob or a variable was mangled by the shell on paste. It now quotes anything outside the shlex-style inert set. --- cli/cmd/dry_run.go | 17 ++++++++++-- cli/cmd/flag_alias.go | 42 ++++++++++++++++++++++++++++ cli/cmd/flag_alias_test.go | 56 ++++++++++++++++++++++++++++++++++++++ cli/cmd/rerun_test.go | 28 +++++++++++++++++++ cli/cmd/scan.go | 8 ++---- cli/cmd/test.go | 4 +-- cli/cmd/test_rule_run.go | 4 +-- 7 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 cli/cmd/flag_alias.go create mode 100644 cli/cmd/flag_alias_test.go diff --git a/cli/cmd/dry_run.go b/cli/cmd/dry_run.go index 39d483a55..914db0609 100644 --- a/cli/cmd/dry_run.go +++ b/cli/cmd/dry_run.go @@ -34,10 +34,23 @@ func rerunWithoutDryRun() string { } // shellQuote single-quotes an argument that would break when copy-pasted into -// a shell (spaces, quotes). Plain arguments pass through unchanged. +// a shell. Only arguments made of known-inert characters pass through +// unchanged, so globs, variables, and separators survive the round trip. func shellQuote(arg string) string { - if arg != "" && !strings.ContainsAny(arg, " \t'\"") { + if arg != "" && !strings.ContainsFunc(arg, shellUnsafe) { return arg } return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'" } + +// shellUnsafe reports whether a character can change the meaning of an +// unquoted shell word. The safe set mirrors Python's shlex.quote. +func shellUnsafe(r rune) bool { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9': + return false + case strings.ContainsRune("_@%+=:,./-", r): + return false + } + return true +} diff --git a/cli/cmd/flag_alias.go b/cli/cmd/flag_alias.go new file mode 100644 index 000000000..30d86fe23 --- /dev/null +++ b/cli/cmd/flag_alias.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "strings" + + "github.com/spf13/pflag" +) + +// renamedStringArray backs a renamed flag and its deprecated alias. pflag's +// stringArrayValue replaces the bound slice on each flag's own first value, so +// two stock flags bound to one slice silently drop whatever the other spelling +// already collected. Appending unconditionally keeps the values of both +// spellings, in command-line order. +type renamedStringArray struct { + target *[]string +} + +func (v renamedStringArray) String() string { + if len(*v.target) == 0 { + return "" + } + return "[" + strings.Join(*v.target, ",") + "]" +} + +func (v renamedStringArray) Set(s string) error { + *v.target = append(*v.target, s) + return nil +} + +func (v renamedStringArray) Type() string { + return "stringArray" +} + +// addRenamedStringArrayFlag registers a flag under its new name and its +// deprecated old spelling, both accumulating into the same slice. +func addRenamedStringArrayFlag(fs *pflag.FlagSet, target *[]string, name, deprecated, usage string) { + fs.Var(renamedStringArray{target}, name, usage) + fs.Var(renamedStringArray{target}, deprecated, usage) + if err := fs.MarkDeprecated(deprecated, "use --"+name); err != nil { + panic(err) + } +} diff --git a/cli/cmd/flag_alias_test.go b/cli/cmd/flag_alias_test.go new file mode 100644 index 000000000..f0220ad99 --- /dev/null +++ b/cli/cmd/flag_alias_test.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "reflect" + "testing" + + "github.com/spf13/pflag" +) + +func parseRenamed(t *testing.T, args []string) []string { + t.Helper() + var target []string + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addRenamedStringArrayFlag(fs, &target, "passthrough-models", "passthrough-approximations", "usage") + if err := fs.Parse(args); err != nil { + t.Fatalf("parse %v: %v", args, err) + } + return target +} + +func TestRenamedFlagAccumulatesAcrossBothSpellings(t *testing.T) { + cases := [][]string{ + {"--passthrough-models", "a.yaml", "--passthrough-approximations", "b.yaml"}, + {"--passthrough-approximations", "a.yaml", "--passthrough-models", "b.yaml"}, + } + for _, args := range cases { + got := parseRenamed(t, args) + if len(got) != 2 { + t.Errorf("args %v: got %v, want both values kept", args, got) + } + } +} + +func TestRenamedFlagKeepsRepeatsInOrder(t *testing.T) { + got := parseRenamed(t, []string{ + "--passthrough-models", "a.yaml", + "--passthrough-models", "b.yaml", + "--passthrough-approximations", "c.yaml", + }) + if want := []string{"a.yaml", "b.yaml", "c.yaml"}; !reflect.DeepEqual(got, want) { + t.Errorf("got %v, want %v", got, want) + } +} + +func TestRenamedFlagAliasIsDeprecatedAndHidden(t *testing.T) { + var target []string + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + addRenamedStringArrayFlag(fs, &target, "passthrough-models", "passthrough-approximations", "usage") + alias := fs.Lookup("passthrough-approximations") + if alias == nil || alias.Deprecated == "" { + t.Fatal("alias must be registered and marked deprecated") + } + if fs.Lookup("passthrough-models").Deprecated != "" { + t.Error("the new spelling must not be deprecated") + } +} diff --git a/cli/cmd/rerun_test.go b/cli/cmd/rerun_test.go index 310b263b6..2d2291f24 100644 --- a/cli/cmd/rerun_test.go +++ b/cli/cmd/rerun_test.go @@ -113,3 +113,31 @@ func TestRetrySuggestion(t *testing.T) { t.Fatal("exception exit code must not produce a retry suggestion") } } + +func TestShellQuoteLeavesInertArgumentsAlone(t *testing.T) { + for _, arg := range []string{"opentaint", "report.sarif", "--max-memory=8G", "path/to/file.yaml", "a-b_c.d,e:f@g%h+i"} { + if got := shellQuote(arg); got != arg { + t.Errorf("shellQuote(%q) = %q, want unchanged", arg, got) + } + } +} + +func TestShellQuoteQuotesShellMetacharacters(t *testing.T) { + cases := map[string]string{ + "demo-rule-*": "'demo-rule-*'", + "java/security/**": "'java/security/**'", + "$HOME": "'$HOME'", + "a;b": "'a;b'", + "a|b": "'a|b'", + "a b": "'a b'", + "it's": `'it'\''s'`, + "": "''", + "a>b": "'a>b'", + "`cmd`": "'`cmd`'", + } + for in, want := range cases { + if got := shellQuote(in); got != want { + t.Errorf("shellQuote(%q) = %q, want %q", in, got, want) + } + } +} diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 85bc267dd..4f266d8bb 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -158,13 +158,9 @@ func addScanFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&scanFlags.ProjectModelPath, "project-model", "", "Path to a pre-compiled project model (skips compilation)") cmd.Flags().StringVar(&scanFlags.LogFile, "log-file", "", "Path to the log file (default: /logs/.log)") - cmd.Flags().StringArrayVar(&scanFlags.PassthroughApproximations, "passthrough-models", nil, "Pass-through models: a YAML file or a directory of them (repeatable)") - cmd.Flags().StringArrayVar(&scanFlags.PassthroughApproximations, "passthrough-approximations", nil, "Pass-through models: a YAML file or a directory of them (repeatable)") - _ = cmd.Flags().MarkDeprecated("passthrough-approximations", "use --passthrough-models") + addRenamedStringArrayFlag(cmd.Flags(), &scanFlags.PassthroughApproximations, "passthrough-models", "passthrough-approximations", "Pass-through models: a YAML file or a directory of them (repeatable)") - cmd.Flags().StringArrayVar(&scanFlags.DataflowApproximations, "java-models", nil, "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") - cmd.Flags().StringArrayVar(&scanFlags.DataflowApproximations, "dataflow-approximations", nil, "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") - _ = cmd.Flags().MarkDeprecated("dataflow-approximations", "use --java-models") + addRenamedStringArrayFlag(cmd.Flags(), &scanFlags.DataflowApproximations, "java-models", "dataflow-approximations", "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") cmd.Flags().BoolVar(&scanFlags.TrackExternalMethods, "track-external-methods", false, "Write external-method coverage files next to the SARIF report") } diff --git a/cli/cmd/test.go b/cli/cmd/test.go index 8598c601b..05ea6d546 100644 --- a/cli/cmd/test.go +++ b/cli/cmd/test.go @@ -40,7 +40,5 @@ func addTestRunFlags(cmd *cobra.Command, outputDir *string, timeout *time.Durati cmd.Flags().StringVarP(outputDir, "output", "o", "", "Directory for test-result.json and test-results.sarif") cmd.Flags().DurationVar(timeout, "timeout", 600*time.Second, "Maximum wall-clock time for analysis (e.g. 30m, 1h)") cmd.Flags().StringVar(maxMemory, "max-memory", "8G", "Maximum analyzer heap size (e.g. 8G, 1024m)") - cmd.Flags().StringArrayVar(dataflow, "java-models", nil, "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") - cmd.Flags().StringArrayVar(dataflow, "dataflow-approximations", nil, "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") - _ = cmd.Flags().MarkDeprecated("dataflow-approximations", "use --java-models") + addRenamedStringArrayFlag(cmd.Flags(), dataflow, "java-models", "dataflow-approximations", "Java dataflow models: a compiled class directory or a Java source directory (repeatable)") } diff --git a/cli/cmd/test_rule_run.go b/cli/cmd/test_rule_run.go index f6a1492a8..55e9ddc7b 100644 --- a/cli/cmd/test_rule_run.go +++ b/cli/cmd/test_rule_run.go @@ -206,7 +206,5 @@ func init() { testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleset, "ruleset", nil, "Ruleset to test: a YAML file or a directory of .yml or .yaml files (repeatable)") addTestRunFlags(testRuleRunCmd, &testRulesOutputDir, &testRulesTimeout, &testRulesMaxMemory, &testRulesDataflow) testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleID, "rule-id", nil, "Run only rules with this ID (repeatable)") - testRuleRunCmd.Flags().StringArrayVar(&testRulesPassthrough, "passthrough-models", nil, "Pass-through models: a YAML file or a directory of them (repeatable)") - testRuleRunCmd.Flags().StringArrayVar(&testRulesPassthrough, "passthrough-approximations", nil, "Pass-through models: a YAML file or a directory of them (repeatable)") - _ = testRuleRunCmd.Flags().MarkDeprecated("passthrough-approximations", "use --passthrough-models") + addRenamedStringArrayFlag(testRuleRunCmd.Flags(), &testRulesPassthrough, "passthrough-models", "passthrough-approximations", "Pass-through models: a YAML file or a directory of them (repeatable)") } From 134146db2b1d0f731b6727a8bc70313ba55fbe35 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 24 Aug 2026 10:43:32 +0200 Subject: [PATCH 6/7] docs(cli): rework the command help in simplified technical English Every command's Long text now follows ASD-STE100 style: short sentences, one instruction per sentence, active voice, and simple words. Every command has an Examples section, and each Examples section ends with recipes that show complete use cases as command sequences, from the first scan on a new machine to the rule-test edit loop. The summary, prune, and reachability Shorts are simplified to match, and the docs tables that quote them are synced. --- cli/cmd/compile.go | 17 +++++++++------ cli/cmd/health.go | 20 +++++++++++------ cli/cmd/project.go | 22 ++++++++++++------- cli/cmd/prune.go | 22 +++++++++++-------- cli/cmd/pull.go | 14 +++++++----- cli/cmd/root.go | 7 ++++-- cli/cmd/scan.go | 25 ++++++++++++++------- cli/cmd/summary.go | 26 ++++++++++++---------- cli/cmd/test.go | 25 +++++++++++++++------ cli/cmd/test_approximation_run.go | 18 ++++++++++------ cli/cmd/test_init.go | 36 ++++++++++++++++++++----------- cli/cmd/test_rule_reachability.go | 24 ++++++++++++--------- cli/cmd/test_rule_run.go | 18 ++++++++++------ cli/cmd/update.go | 18 ++++++++++------ docs/installation.md | 2 +- docs/usage.md | 4 ++-- 16 files changed, 190 insertions(+), 108 deletions(-) diff --git a/cli/cmd/compile.go b/cli/cmd/compile.go index dfec1651f..8da1600dc 100644 --- a/cli/cmd/compile.go +++ b/cli/cmd/compile.go @@ -42,18 +42,23 @@ var compileCmd = &cobra.Command{ Use: "compile ", Short: "Compile a project into a reusable project model", Args: cobra.ExactArgs(1), // require exactly one argument - Long: `OpenTaint detects the build system, resolves the project's modules and dependencies, and compiles the project into a reusable model. + Long: `Compile a project into a project model that you can scan many times. OpenTaint finds the build system, collects the modules and dependencies, and builds the project. -The project argument is the path to the project root and is required. Pass --output to name the project model directory to create. It must not already exist. +The project argument is the path to the project root. It is required. Use --output to set the project model directory. This directory must not exist before the command runs. -The project model is written to the --output directory and can be reused by later scans without rebuilding. +Later scans can use the model without a new build. This makes repeated scans fast. -Run opentaint pull once before your first compile to fetch the toolchain. Analyze the resulting project model with opentaint scan --project-model.`, +Before your first compile, run "opentaint pull" one time. To scan the model, use "opentaint scan --project-model".`, Example: ` # Compile the current directory into a project model opentaint compile . -o ./model - # Validate inputs without compiling - opentaint compile . -o ./model --dry-run`, + # Make sure the inputs are correct, without a build + opentaint compile . -o ./model --dry-run + + # Recipe: compile one time, then scan with different settings + opentaint compile ./my-app -o ./model + opentaint scan --project-model ./model --ruleset ./team-rules -o team.sarif + opentaint scan --project-model ./model --severity error -o errors.sarif`, Annotations: map[string]string{"PrintConfig": "true"}, Run: func(cmd *cobra.Command, args []string) { ProjectPath = args[0] diff --git a/cli/cmd/health.go b/cli/cmd/health.go index df3f1b2dc..4878de84d 100644 --- a/cli/cmd/health.go +++ b/cli/cmd/health.go @@ -28,19 +28,25 @@ type healthComponent struct { var healthCmd = &cobra.Command{ Use: "health", Short: "Show dependency paths and report missing components", - Long: `Show the on-disk paths for the autobuilder, analyzer, built-in rules, and Java runtime, and report whether each component is present. + Long: `Show the paths of the components on this computer. The components are the autobuilder, the analyzer, the built-in rules, and the Java runtime. The command shows if each component is present. -Select components with --autobuilder, --analyzer, --rules, or --runtime. With no flag, all four are reported. When exactly one component is selected, only its path is printed, which suits scripting. Only the built-in rules are fetched on demand. No other artifact is downloaded. +To select components, use --autobuilder, --analyzer, --rules, or --runtime. With no flag, the command shows all four components. If you select exactly one component, only its path is printed. This output is good for scripts. -The command exits non-zero when any selected component is missing. Download the missing components with opentaint pull.`, - Example: ` # Report all components and their paths +Only the built-in rules are downloaded when they are missing. No other component is downloaded. + +If a selected component is missing, the command exits with a code that is not zero. To download the missing components, run "opentaint pull".`, + Example: ` # Show all components and their paths opentaint health - # Print only the analyzer JAR path, for scripting + # Print only the analyzer JAR path, for a script opentaint health --analyzer - # Check the Java runtime - opentaint health --runtime`, + # Make sure the Java runtime is present + opentaint health --runtime + + # Recipe: use the built-in rules path in a script + RULES=$(opentaint health --rules) + opentaint scan . --ruleset "$RULES" --ruleset ./extra-rules -o report.sarif`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runHealth() diff --git a/cli/cmd/project.go b/cli/cmd/project.go index 5f3e1d5d6..ed7236c3f 100644 --- a/cli/cmd/project.go +++ b/cli/cmd/project.go @@ -220,21 +220,27 @@ var ( var projectCmd = &cobra.Command{ Use: "project", Short: "Create a project model from precompiled JARs or classes", - Long: `Create a project model directly from precompiled JARs or classes, without running a build. OpenTaint inspects the supplied classpath, detects the modules and dependencies, and writes a project.yaml describing the project for later analysis. Use this when you already have compiled artifacts. To build a model from sources, use opentaint compile instead. + Long: `Create a project model from JARs or classes that are already compiled. No build occurs. OpenTaint examines the classpath, finds the modules and dependencies, and writes a project.yaml file. -All inputs are supplied as flags. Provide --source-root for the project sources, one or more --classpath entries for the compiled classes or JARs, and one or more --package names to include. Add --dependency for extra JAR files on the compile classpath. +Use this command when you have compiled artifacts but no build. To build a model from sources, use "opentaint compile". -The project model is written to the required --output directory, which must not already exist, and contains the generated project.yaml. +All inputs are flags. Give the source path with --source-root. Give the compiled classes or JARs with --classpath. Give the packages to include with --package. Add more JAR files with --dependency. -Run opentaint pull once beforehand to fetch the autobuilder. Scan the generated model with opentaint scan --project-model .`, - Example: ` # Generate a project model from a compiled JAR +Use --output to set the project model directory. This directory must not exist before the command runs. + +Before the first run, run "opentaint pull" one time. To scan the model, use "opentaint scan --project-model".`, + Example: ` # Create a project model from a compiled JAR opentaint project --source-root ./src --classpath ./app.jar --package com.example -o ./model - # Add extra dependency JARs to the classpath + # Add more dependency JARs to the classpath opentaint project --source-root ./src --classpath ./app.jar --dependency ./lib.jar --package com.example -o ./model - # Validate the inputs without generating anything - opentaint project --source-root ./src --classpath ./app.jar --package com.example -o ./model --dry-run`, + # Make sure the inputs are correct, without a model + opentaint project --source-root ./src --classpath ./app.jar --package com.example -o ./model --dry-run + + # Recipe: scan a vendor JAR that you cannot build + opentaint project --source-root ./src --classpath ./vendor-app.jar --package com.vendor -o ./model + opentaint scan --project-model ./model -o report.sarif`, Run: func(cmd *cobra.Command, args []string) { config := NewJavaAutobuilder(). WithOutputDir(OutputDir). diff --git a/cli/cmd/prune.go b/cli/cmd/prune.go index 1fc24300b..aab398e19 100644 --- a/cli/cmd/prune.go +++ b/cli/cmd/prune.go @@ -57,23 +57,27 @@ func resolveCategories() (utils.PruneCategory, error) { var pruneCmd = &cobra.Command{ Use: "prune", - Short: "Remove stale downloaded artifacts from the cache", - Long: `Remove stale downloaded artifacts from the local cache (~/.opentaint): superseded analyzer and autobuilder JARs, old rules, JDK and JRE versions that no longer match the configured one, and cached project models. + Short: "Remove old downloaded artifacts from the cache", + Long: `Remove old downloaded artifacts from the local cache (~/.opentaint). The command removes analyzer and autobuilder JARs that a newer version replaced, old rules, JDK and JRE versions that do not match the configuration, and cached project models. -Select categories with --artifacts, --rules, --jdk, --models, --logs, or --install. Without a category flag, prune removes artifacts, rules, jdk, and models. The --all flag removes everything, including logs and install-tier artifacts, and cannot be combined with a specific category flag. +To select categories, use --artifacts, --rules, --jdk, --models, --logs, or --install. With no category flag, the command removes artifacts, rules, jdk, and models. The --all flag removes everything, with logs and install-tier artifacts included. Do not give --all together with a category flag. -Preview the deletions with --dry-run, and skip the confirmation prompt with --yes. Restore install-tier artifacts afterward with opentaint pull.`, - Example: ` # Prune the default categories after confirming +To see the deletions without a removal, use --dry-run. To skip the confirmation prompt, use --yes. To download the toolchain again, run "opentaint pull".`, + Example: ` # Remove the default categories, with a confirmation prompt opentaint prune - # Prune only old JDK and JRE versions + # Remove only the old JDK and JRE versions opentaint prune --jdk - # Prune everything, including logs and install-tier artifacts + # Remove everything, with logs and install-tier artifacts included opentaint prune --all - # Preview what would be deleted without deleting - opentaint prune --dry-run`, + # See what the command would delete, without a deletion + opentaint prune --dry-run + + # Recipe: get disk space back, keep the current toolchain + opentaint prune --dry-run + opentaint prune --yes`, Run: func(cmd *cobra.Command, args []string) { categories, err := resolveCategories() if err != nil { diff --git a/cli/cmd/pull.go b/cli/cmd/pull.go index d73b5fa38..8dfbb3e39 100644 --- a/cli/cmd/pull.go +++ b/cli/cmd/pull.go @@ -18,16 +18,20 @@ import ( var pullCmd = &cobra.Command{ Use: "pull", Short: "Download the analysis toolchain and Java runtime", - Long: `Download the analyzer, autobuilder, built-in rules, go-ssa-server, and a bundled Java runtime into the local cache. OpenTaint uses these to build and analyze projects without further network access. + Long: `Download the toolchain into the local cache. The toolchain contains the analyzer, the autobuilder, the built-in rules, the go-ssa-server, and a Java runtime. After the download, OpenTaint can build and scan projects without network access. -When bundled artifacts from a release archive are present, they are used directly instead of downloading. +If a release archive supplied bundled artifacts, OpenTaint uses them. They are not downloaded again. -Run opentaint pull once before your first scan. Remove stale downloads later with opentaint prune.`, +Run "opentaint pull" one time before your first scan. To remove old downloads, use "opentaint prune".`, Example: ` # Download the toolchain before the first scan opentaint pull - # Fetch a different Java runtime version - opentaint pull --java-version 17`, + # Download a different Java runtime version + opentaint pull --java-version 17 + + # Recipe: prepare a machine that will have no network access + opentaint pull + opentaint health`, Run: func(cmd *cobra.Command, args []string) { out.Section("OpenTaint Pull"). Field("Autobuilder", globals.Config.Autobuilder.Version). diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 85a223926..19a90e4b7 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -35,9 +35,12 @@ var updateHintCh = make(chan string, 1) var rootCmd = &cobra.Command{ Use: "opentaint", Short: "Find vulnerabilities in source code with taint analysis", - Long: `OpenTaint finds vulnerabilities by tracing tainted data from untrusted sources to sensitive sinks. Java, Kotlin, and Go projects are supported. + Long: `OpenTaint finds vulnerabilities in your code. It follows tainted data from untrusted sources to dangerous sinks. Java, Kotlin, and Go projects are supported. -Run opentaint pull once to fetch the toolchain, opentaint scan to analyze a project, and opentaint summary to re-inspect a SARIF report.`, +Quick start: + 1. Run "opentaint pull" one time. This downloads the toolchain. + 2. Run "opentaint scan ." to scan a project. + 3. Run "opentaint summary --show-findings" to read the findings.`, SilenceErrors: true, SilenceUsage: true, diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 4f266d8bb..7b7af94af 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -75,29 +75,38 @@ var scanCmd = &cobra.Command{ Use: "scan [source-path]", Short: "Scan a project for vulnerabilities", Args: cobra.MaximumNArgs(1), - Long: `Scan a project for vulnerabilities. OpenTaint detects the build system, builds the project, and runs taint analysis over the result. + Long: `Scan a project and find vulnerabilities. OpenTaint finds the build system, builds the project, and does a taint analysis. -The optional source-path argument is the project root and defaults to the current directory. Pass --project-model to analyze a pre-compiled project model instead of building. The source-path argument and --project-model are mutually exclusive. +The source-path argument is the project root. It is optional. The default is the current directory. To scan a project model that is already compiled, use --project-model. Do not give source-path and --project-model together. -Findings are written as a SARIF report to --output, or into the project model directory when unset, and summarized on completion. +OpenTaint writes the findings to a SARIF report. Use --output to set the report path. If --output is not set, the report goes into the project model directory. A summary is shown when the scan completes. -Run opentaint pull once before your first scan to fetch the toolchain. Re-inspect a report later with opentaint summary. +Before your first scan, run "opentaint pull" one time. To read a report again later, use "opentaint summary". ` + scanExitCodesHelp("Scan completed"), Example: ` # Scan the current directory with the built-in rules opentaint scan . - # Scan a project and write a SARIF report + # Scan a project and write the report to a known path opentaint scan ./my-app -o report.sarif - # Analyze a pre-compiled project model instead of building + # Scan a project model that is already compiled opentaint scan --project-model ./model -o report.sarif - # Run a custom ruleset and report only errors + # Use your own rules and show only errors opentaint scan . --ruleset ./rules --severity error -o report.sarif # Give a large project more time and memory - opentaint scan . --timeout 30m --max-memory 16G -o report.sarif`, + opentaint scan . --timeout 30m --max-memory 16G -o report.sarif + + # Recipe: first scan on a new machine + opentaint pull + opentaint scan . -o report.sarif + opentaint summary report.sarif --show-findings + + # Recipe: build one time, then scan many times + opentaint compile ./my-app -o ./model + opentaint scan --project-model ./model -o report.sarif`, Annotations: map[string]string{"PrintConfig": "true"}, Run: func(cmd *cobra.Command, args []string) { if scanFlags.DebugRunAnalysisOnSelectedEntryPoints != "" { diff --git a/cli/cmd/summary.go b/cli/cmd/summary.go index 7be7d9f7d..9b6da780c 100644 --- a/cli/cmd/summary.go +++ b/cli/cmd/summary.go @@ -10,29 +10,33 @@ import ( // summaryCmd represents the summary command var summaryCmd = &cobra.Command{ Use: "summary ", - Short: "Summarize a SARIF report", + Short: "Show a summary of a SARIF report", Args: cobra.ExactArgs(1), // require exactly one argument - Long: `Summarize a SARIF report on the terminal. OpenTaint counts the findings by severity, groups them, and shows which rules ran and which produced results. + Long: `Show a summary of a SARIF report in the terminal. The summary counts the findings by severity. It also shows which rules ran and which rules found problems. -The required positional argument is the path to a SARIF report, such as one written by opentaint scan or opentaint test. Pass --show-findings to list every finding. Narrow the listing with --severity, --rule-id, or --path, and expand code flows with --show-code-snippets and --verbose-flow. +The sarif-report argument is the path to a SARIF report. It is required. Use a report from "opentaint scan" or "opentaint test". -The report is read only: the summary and any findings are printed to the terminal and nothing is written to disk. +To see each finding, use --show-findings. To make the list smaller, use --severity, --rule-id, or --path. To see the full data flow, use --verbose-flow and --show-code-snippets. -Run opentaint scan to produce the report this command reads.`, - Example: ` # Print a summary of a report +This command only reads the report. It does not write files.`, + Example: ` # Show a summary of a report opentaint summary report.sarif - # List every finding with its location + # Show each finding with its location opentaint summary report.sarif --show-findings - # Show only error-level findings + # Show only the error-level findings opentaint summary report.sarif --show-findings --severity error - # Group the listing by rule + # Group the findings by rule opentaint summary report.sarif --show-findings --group-by rule-id - # Trace one rule with full code flow and snippets - opentaint summary report.sarif --show-findings --rule-id --show-code-snippets --verbose-flow`, + # Recipe: examine one rule in full detail + opentaint summary report.sarif --show-findings --group-by rule-id + opentaint summary report.sarif --show-findings --rule-id --verbose-flow --show-code-snippets + + # Recipe: read the findings for one part of the code + opentaint summary report.sarif --show-findings --path "src/main/**" --severity error`, Run: func(cmd *cobra.Command, args []string) { for _, s := range summarySeverities { diff --git a/cli/cmd/test.go b/cli/cmd/test.go index 05ea6d546..6b3b25a55 100644 --- a/cli/cmd/test.go +++ b/cli/cmd/test.go @@ -9,25 +9,38 @@ import ( var testCmd = &cobra.Command{ Use: "test", Short: "Create and run rule and approximation tests", - Long: `Create, run, and debug rule and approximation tests. Rule tests check detection rules against annotated sample projects. Approximation tests check dataflow approximations the same way. + Long: `Create and run tests for detection rules and for dataflow approximations. Rule tests make sure that a rule finds the positive samples and ignores the negative samples. Approximation tests make sure that a dataflow approximation moves taint from source to sink. -Scaffold a project with init, compile it with opentaint compile, then run the samples with test rule run or test approximation run. Use test rule reachability to debug why a single rule does or does not fire.`, +Workflow: + 1. Create a test project with init. + 2. Compile the project with "opentaint compile". + 3. Run the samples with "test rule run" or "test approximation run". + +To see why one rule does or does not fire, use "test rule reachability".`, } var testRuleCmd = &cobra.Command{ Use: "rule", Short: "Create, run, and debug detection-rule tests", - Long: `Create, run, and debug taint detection-rule tests. Rule tests check that a rule fires on positive samples and stays silent on negative ones. + Long: `Create, run, and debug tests for taint detection rules. A rule test makes sure that a rule finds the positive samples and ignores the negative samples. + +Workflow: + 1. Create a test project with "test rule init". + 2. Compile the project with "opentaint compile". + 3. Run the samples with "test rule run". -Scaffold a test project with test rule init, compile it with opentaint compile, then run the samples with test rule run. Use test rule reachability to trace why a single rule does or does not fire.`, +To see why one rule does or does not fire, use "test rule reachability".`, } var testApproximationCmd = &cobra.Command{ Use: "approximation", Short: "Create and run dataflow-approximation tests", - Long: `Create and run dataflow-approximation tests. Approximation tests check that a dataflow approximation carries taint from source to sink across your samples. + Long: `Create and run tests for dataflow approximations. An approximation test makes sure that an approximation moves taint from source to sink in your samples. -Scaffold a test project with test approximation init, compile it with opentaint compile, then run the samples with test approximation run, supplying the approximation under test with --java-models.`, +Workflow: + 1. Create a test project with "test approximation init". + 2. Compile the project with "opentaint compile". + 3. Run the samples with "test approximation run --java-models ".`, } func init() { diff --git a/cli/cmd/test_approximation_run.go b/cli/cmd/test_approximation_run.go index 6bb796a3a..9f688c1f4 100644 --- a/cli/cmd/test_approximation_run.go +++ b/cli/cmd/test_approximation_run.go @@ -18,20 +18,24 @@ var ( var testApproximationRunCmd = &cobra.Command{ Use: "run ", Short: "Run dataflow-approximation tests on a compiled project model", - Long: `Run the samples declared in rule-test.yaml with the supplied dataflow approximations applied and report which passed. A fixed source-to-sink harness rule is applied automatically. Positive samples reference it by id approximation-rule. + Long: `Run the samples that rule-test.yaml declares, with your dataflow approximations applied. The command reports which samples passed. A fixed source-to-sink harness rule is applied automatically. Positive samples point to it with the id approximation-rule. -The project-model argument is a compiled project model directory, produced by opentaint compile. Supply the approximation under test with --java-models. +The project-model argument is a compiled project model directory from "opentaint compile". Give the approximation under test with --java-models. -Results are written as test-result.json and a test-results.sarif report to --output, or to a temporary directory when unset. +The command writes test-result.json and a test-results.sarif report to --output. If --output is not set, it writes to a temporary directory. -Compile the test project with opentaint compile before running. Inspect the results afterward with opentaint summary. +Compile the test project before you run the tests. To read the results, use "opentaint summary". ` + testExitCodesHelp("All approximation tests passed"), - Example: ` # Run an approximation test against a compiled model + Example: ` # Run an approximation test on a compiled model opentaint test approximation run ./approx-test/model --java-models ./approx - # Write results to a directory - opentaint test approximation run ./approx-test/model --java-models ./approx -o ./results`, + # Write the results to a directory + opentaint test approximation run ./approx-test/model --java-models ./approx -o ./results + + # Recipe: change an approximation, then make sure the tests stay green + opentaint test approximation run ./approx-test/model --java-models ./approx -o ./results + opentaint summary ./results/test-results.sarif --show-findings`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { ruleDir, err := os.MkdirTemp("", "opentaint-approx-rule-*") diff --git a/cli/cmd/test_init.go b/cli/cmd/test_init.go index c947ad9aa..0686204fb 100644 --- a/cli/cmd/test_init.go +++ b/cli/cmd/test_init.go @@ -19,21 +19,26 @@ var initRuleSourcesOnly bool var testRuleInitCmd = &cobra.Command{ Use: "init ", Short: "Create rule test projects with source and sink harnesses", - Long: `Create one or two Gradle test projects for detection-rule tests. The sinks project tests sink rules against a generic taint source. The sources project tests source rules against a generic taint sink. + Long: `Create one or two Gradle test projects for detection-rule tests. The sinks project tests sink rules with a generic taint source. The sources project tests source rules with a generic taint sink. -The output-dir argument is the parent directory the projects are created under. By default both are scaffolded, as output-dir/sinks and output-dir/sources. Pass --sinks-only or --sources-only to create just one. Use --dependency to add compile-only Maven dependencies for the samples. +The output-dir argument is the parent directory for the new projects. The default creates the two projects, in output-dir/sinks and output-dir/sources. To create one project only, use --sinks-only or --sources-only. To add compile-only Maven dependencies for the samples, use --dependency. -Each project ships a rule-test.yaml where you declare the positive and negative samples, plus a Taint.java source and sink harness. +Each project contains a rule-test.yaml file and a Taint.java harness. Declare your positive and negative samples in rule-test.yaml. -After editing rule-test.yaml, compile the project with opentaint compile and run the samples with opentaint test rule run.`, - Example: ` # Scaffold both the sinks and sources test projects +Then compile the project with "opentaint compile" and run the samples with "opentaint test rule run".`, + Example: ` # Create the sinks and the sources test projects opentaint test rule init ./rule-tests - # Scaffold only the sinks project + # Create only the sinks project opentaint test rule init ./rule-tests --sinks-only # Add a compile-only dependency for the samples - opentaint test rule init ./rule-tests --dependency `, + opentaint test rule init ./rule-tests --dependency + + # Recipe: from an empty directory to a first test run + opentaint test rule init ./rule-tests + opentaint compile ./rule-tests/sinks -o ./rule-tests/sinks/model + opentaint test rule run ./rule-tests/sinks/model --ruleset ./my-rules`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { if initRuleSinksOnly && initRuleSourcesOnly { @@ -68,18 +73,23 @@ After editing rule-test.yaml, compile the project with opentaint compile and run var testApproximationInitCmd = &cobra.Command{ Use: "init ", Short: "Create a dataflow-approximation test project", - Long: `Create a Gradle test project for dataflow-approximation tests. The project pins a fixed source-to-sink rule that the samples are checked against. + Long: `Create a Gradle test project for dataflow-approximation tests. The project contains a fixed source-to-sink rule. The samples are checked against this rule. -The output-dir argument is the directory the project is created in. Use --dependency to add compile-only Maven dependencies for the samples. The approximation under test is not baked in. Supply it at run time with --java-models. +The output-dir argument is the directory for the new project. To add compile-only Maven dependencies for the samples, use --dependency. The approximation under test is not part of the project. Give it at run time with --java-models. -The project ships a rule-test.yaml where you declare the positive and negative samples, plus a Taint.java source and sink and the fixed approximation-rule.yaml. +The project contains a rule-test.yaml file, a Taint.java source and sink, and the fixed approximation-rule.yaml. Declare your positive and negative samples in rule-test.yaml. -After editing rule-test.yaml, compile the project with opentaint compile and run the samples with opentaint test approximation run.`, - Example: ` # Scaffold an approximation test project +Then compile the project with "opentaint compile" and run the samples with "opentaint test approximation run".`, + Example: ` # Create an approximation test project opentaint test approximation init ./approx-test # Add a compile-only dependency for the samples - opentaint test approximation init ./approx-test --dependency `, + opentaint test approximation init ./approx-test --dependency + + # Recipe: from an empty directory to a first test run + opentaint test approximation init ./approx-test + opentaint compile ./approx-test -o ./approx-test/model + opentaint test approximation run ./approx-test/model --java-models ./my-approximation`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { if err := testproject.Bootstrap(args[0], "approximation-test-project", initApproxProjectDeps); err != nil { diff --git a/cli/cmd/test_rule_reachability.go b/cli/cmd/test_rule_reachability.go index 5d61019b2..20714bdf8 100644 --- a/cli/cmd/test_rule_reachability.go +++ b/cli/cmd/test_rule_reachability.go @@ -8,27 +8,31 @@ var reachabilityEntryPoint string var testRuleReachabilityCmd = &cobra.Command{ Use: "reachability [source-path]", - Short: "Trace why a rule can or cannot reach its facts", - Long: `Scan a project with a single rule and write a fact-reachability SARIF report so you can see why that rule does or does not fire. Referenced library source and sink rules are collected and analyzed automatically. + Short: "Show why a rule does or does not fire", + Long: `Scan a project with one rule and write a fact-reachability SARIF report. The report shows why the rule does or does not fire. Library source and sink rules that the rule points to are included automatically. -The rule-id argument selects the one rule to trace. The optional source-path argument is the project root and defaults to the current directory. Pass --project-model to trace a pre-compiled model instead. The source-path argument and --project-model are mutually exclusive. Use --entry-points to start the analysis from a specific method. +The rule-id argument selects the rule. The source-path argument is the project root. It is optional. The default is the current directory. To use a compiled model, use --project-model. Do not give source-path and --project-model together. To start the analysis from one method, use --entry-points. -The report is written as debug-ifds-fact-reachability.sarif next to the main SARIF report. +The report name is debug-ifds-fact-reachability.sarif. It is written adjacent to the main SARIF report. -Run opentaint pull once before your first run to fetch the toolchain. Open the reachability report afterward with opentaint summary. +Before the first run, run "opentaint pull" one time. To read the report, use "opentaint summary". ` + scanExitCodesHelp("Reachability analysis completed"), - Example: ` # Trace a rule against the current directory + Example: ` # Show why a rule does or does not fire on the current directory opentaint test rule reachability . - # Trace a rule against a pre-compiled project model + # Examine a rule on a compiled project model opentaint test rule reachability --project-model ./model - # Start the analysis from a specific entry-point method + # Start the analysis from one entry-point method opentaint test rule reachability . --entry-points com.example.App#main - # Validate inputs without compiling or scanning - opentaint test rule reachability . --dry-run`, + # Make sure the inputs are correct, without a scan + opentaint test rule reachability . --dry-run + + # Recipe: find why a new rule stays silent + opentaint test rule reachability . -o report.sarif + opentaint summary debug-ifds-fact-reachability.sarif --show-findings --verbose-flow`, Annotations: map[string]string{"PrintConfig": "true"}, Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { diff --git a/cli/cmd/test_rule_run.go b/cli/cmd/test_rule_run.go index 55e9ddc7b..15a1e3c4b 100644 --- a/cli/cmd/test_rule_run.go +++ b/cli/cmd/test_rule_run.go @@ -26,23 +26,27 @@ var ( var testRuleRunCmd = &cobra.Command{ Use: "run ", Short: "Run detection-rule tests on a compiled project model", - Long: `Run detection rules against the samples declared in rule-test.yaml and report which passed. The built-in rules are always included. + Long: `Run detection rules on the samples that rule-test.yaml declares. The command reports which samples passed. The built-in rules are always included. -The project-model argument is a compiled project model directory, produced by opentaint compile. Add your own rules with --ruleset, narrow the run to specific rules with --rule-id, and apply models with --java-models or --passthrough-models. +The project-model argument is a compiled project model directory from "opentaint compile". To add your own rules, use --ruleset. To run only specified rules, use --rule-id. To apply models, use --java-models or --passthrough-models. -Results are written as test-result.json and a test-results.sarif report to --output, or to a temporary directory when unset. +The command writes test-result.json and a test-results.sarif report to --output. If --output is not set, it writes to a temporary directory. -Compile the test project with opentaint compile before running. Inspect the results afterward with opentaint summary. +Compile the test project before you run the tests. To read the results, use "opentaint summary". ` + testExitCodesHelp("All rule tests passed"), - Example: ` # Run the built-in rules against a compiled model + Example: ` # Run the built-in rules on a compiled model opentaint test rule run ./rule-tests/sinks/model - # Test a custom ruleset and write results to a directory + # Test your own rules and write the results to a directory opentaint test rule run ./rule-tests/sinks/model --ruleset ./rules -o ./results # Run only one rule - opentaint test rule run ./rule-tests/sinks/model --rule-id `, + opentaint test rule run ./rule-tests/sinks/model --rule-id + + # Recipe: change a rule, then make sure the tests stay green + opentaint test rule run ./rule-tests/sinks/model --ruleset ./rules -o ./results + opentaint summary ./results/test-results.sarif --show-findings`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { runTestProject(args[0], testProjectOptions{ diff --git a/cli/cmd/update.go b/cli/cmd/update.go index 19d83a8e4..6ef0484d9 100644 --- a/cli/cmd/update.go +++ b/cli/cmd/update.go @@ -20,19 +20,25 @@ var ( var updateCmd = &cobra.Command{ Use: "update [version]", Short: "Update opentaint to the latest version", - Long: `Update the opentaint binary in place to the latest release, or to the optional version argument. Only upgrades are supported. Downgrading to an older version is refused. + Long: `Update the opentaint binary to the latest release. To get a specified version, give the version argument. Only upgrades are possible. The command refuses a version that is older than the current one. -Homebrew and npm installations print the matching package-manager command instead of updating in place. Pass --check to report the latest version without downloading, or --yes to skip the confirmation prompt. +If opentaint was installed with Homebrew or npm, the command does not change the binary. It shows the correct package-manager command. -After a successful update, remove superseded artifacts with opentaint prune.`, +To see the latest version without a download, use --check. To skip the confirmation prompt, use --yes. + +After a successful update, remove the old artifacts with "opentaint prune".`, Example: ` # Update to the latest release opentaint update - # Check for a newer version without installing + # See if a newer version is available, without a download opentaint update --check - # Update to a specific version without prompting - opentaint update 1.2.3 --yes`, + # Update to a specified version without a prompt + opentaint update 1.2.3 --yes + + # Recipe: update, then remove the artifacts of the old version + opentaint update --yes + opentaint prune --yes`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { // Check installation method first diff --git a/docs/installation.md b/docs/installation.md index 67ecb6ac8..34fdf5971 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -150,7 +150,7 @@ For package manager installations, `opentaint update` will show the appropriate ## Cleaning Up -Remove stale downloaded artifacts: +Remove old downloaded artifacts: ```bash opentaint prune # Interactive confirmation diff --git a/docs/usage.md b/docs/usage.md index 00089ab0d..bbf0f4600 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -87,7 +87,7 @@ Use [CodeChecker](https://github.com/Ericsson/codechecker) for advanced result m | `opentaint test approximation` | Create and run dataflow-approximation tests | | `opentaint pull` | Download the analysis toolchain and Java runtime | | `opentaint update` | Update to latest version | -| `opentaint prune` | Remove stale downloaded artifacts and cached models | +| `opentaint prune` | Remove old downloaded artifacts and cached models | ### opentaint scan @@ -155,7 +155,7 @@ opentaint test rule reachability java/security/my-rule.yaml:my-rule --project-mo |---------|-------------| | `opentaint test rule init ` | Create source and sink test projects with annotated sample support | | `opentaint test rule run ` | Run detection-rule tests on a compiled project model | -| `opentaint test rule reachability [source-path]` | Trace why a rule can or cannot reach its facts | +| `opentaint test rule reachability [source-path]` | Show why a rule does or does not fire | #### Approximation tests From f79367d6a15c853c2999e0893a98fbff7f32b5e4 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Tue, 25 Aug 2026 03:44:18 +0200 Subject: [PATCH 7/7] docs(cli): keep Go support out of this branch's help and docs The help rewrite mentioned Go in the root and pull help, and the docs mentioned go.mod, the go-ssa-server, and the Go toolchain. Go support lands in its own branch. That branch states it, in its own words. --- cli/cmd/pull.go | 2 +- cli/cmd/root.go | 2 +- docs/installation.md | 2 +- docs/usage.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/cmd/pull.go b/cli/cmd/pull.go index 8dfbb3e39..503ab2442 100644 --- a/cli/cmd/pull.go +++ b/cli/cmd/pull.go @@ -18,7 +18,7 @@ import ( var pullCmd = &cobra.Command{ Use: "pull", Short: "Download the analysis toolchain and Java runtime", - Long: `Download the toolchain into the local cache. The toolchain contains the analyzer, the autobuilder, the built-in rules, the go-ssa-server, and a Java runtime. After the download, OpenTaint can build and scan projects without network access. + Long: `Download the toolchain into the local cache. The toolchain contains the analyzer, the autobuilder, the built-in rules, and a Java runtime. After the download, OpenTaint can build and scan projects without network access. If a release archive supplied bundled artifacts, OpenTaint uses them. They are not downloaded again. diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 19a90e4b7..e457002da 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -35,7 +35,7 @@ var updateHintCh = make(chan string, 1) var rootCmd = &cobra.Command{ Use: "opentaint", Short: "Find vulnerabilities in source code with taint analysis", - Long: `OpenTaint finds vulnerabilities in your code. It follows tainted data from untrusted sources to dangerous sinks. Java, Kotlin, and Go projects are supported. + Long: `OpenTaint finds vulnerabilities in your code. It follows tainted data from untrusted sources to dangerous sinks. Java and Kotlin projects are supported. Quick start: 1. Run "opentaint pull" one time. This downloads the toolchain. diff --git a/docs/installation.md b/docs/installation.md index 34fdf5971..8d76abffe 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,6 +1,6 @@ # Installation -**Prerequisites:** Same build requirements as your project (Maven or Gradle for Java/Kotlin, the Go toolchain for Go modules). Java runtime is bundled with release archives. +**Prerequisites:** Same build requirements as your project (Maven or Gradle for Java/Kotlin). Java runtime is bundled with release archives. ## Homebrew (Linux/macOS) diff --git a/docs/usage.md b/docs/usage.md index bbf0f4600..7a60d4812 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -91,7 +91,7 @@ Use [CodeChecker](https://github.com/Ericsson/codechecker) for advanced result m ### opentaint scan -Automatically detects the project's build system (Maven, Gradle, or go.mod), builds the project, and runs taint analysis over the result. The source path defaults to the current directory when omitted. +Automatically detects the project's build system (Maven or Gradle), builds the project, and runs taint analysis over the result. The source path defaults to the current directory when omitted. On the first run, the compiled project model is cached in `~/.opentaint/cache/`. Subsequent scans of the same project reuse the cached model, skipping compilation entirely. @@ -175,7 +175,7 @@ Rule and approximation test runs write `test-result.json` and `test-results.sari ### opentaint compile -Compiles Java, Kotlin, and Go projects and generates project models for analysis. Useful when you want to separate compilation from scanning or need to inspect the project model. +Compiles Java and Kotlin projects and generates project models for analysis. Useful when you want to separate compilation from scanning or need to inspect the project model. ```bash opentaint compile --output ./my-project-model /path/to/project