From 7a81b4f0402628e2b250f804332b8dd29d7f2bf9 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sun, 23 Aug 2026 23:35:55 +0200 Subject: [PATCH 1/8] 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 3d33c8d337..965b3973f8 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 e0c2f7276a..c6086cb4e6 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 0000000000..5de9a0912e --- /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 0954bbbf47..4e3de53bd6 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 60e31d5d20..0e8ff5cd23 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 a4da4e6b84..162a3a4c27 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 ff95db860f..d73b5fa38f 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 0000000000..310b263b6d --- /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 214028ec8b..85a2239267 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 9750dcdeb9..8c88b43747 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 9508a86cb0..416c76e694 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 0959b5257c..58231fa4ec 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 409240606c..8c815e022e 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 018e16c03d..ca0210e9f3 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 2c4966f822..a14105bc6d 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 ecb0bf3231..58bfc62333 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 4b380c617b..83dbc3b6e7 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 1d3a5c1bda..a1dd5ef0e8 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 6d38e75992..9e97afbdad 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 760695b752..cb3fe42fdf 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 c1e400cc8f..67ecb6ac87 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 84f9359ed2..6f777013ea 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 6ff82f3e10e82f4051343604368087dfb338d07f Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sun, 23 Aug 2026 23:36:13 +0200 Subject: [PATCH 2/8] 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 3290a995fa..3ed3a45711 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 ed61db7b1f..651d5e3c12 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 8c88b43747..4b6a996418 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 8c815e022e..0ff6670ee3 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 ca0210e9f3..a8ca1d7df1 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 a14105bc6d..22ab6b66e7 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 83dbc3b6e7..ef6b8484a6 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 1356a557bb..369f62cd51 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 6f777013ea..fcaf3ad2f9 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 523eb6e5b7..943916079e 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 dd8fce4a05..bd27742644 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 4c4a933ff9..b3ebffeb06 100644 --- a/skills-templates/create-rule/sections/workflow.md +++ b/skills-templates/create-rule/sections/workflow.md @@ -28,7 +28,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 3a490974f6..888b296d24 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 4ea2d1f03b..738bb00f40 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 executable rules (repeatable, one per input rule ID); referenced library rules remain available without listing their IDs separately. 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 8b7ce31714..8d80bd6ce8 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 8b7ce31714..8d80bd6ce8 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 523eb6e5b7..943916079e 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 9f8ae12700..1e20b877b1 100644 --- a/skills/create-rule/SKILL.md +++ b/skills/create-rule/SKILL.md @@ -55,7 +55,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 e6d8f8ad17..b280d46ea7 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 0907d496d4..d5a1d464b2 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 5425cb4cbf..0e748a2379 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 executable rules (repeatable, one per input rule ID); referenced library rules remain available without listing their IDs separately. 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 07bef16fb71cd83be2e9c437929c610694ea427f Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 24 Aug 2026 01:01:08 +0200 Subject: [PATCH 3/8] 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 965b3973f8..dfec1651f4 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 c6086cb4e6..39d483a556 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 4e3de53bd6..df3f1b2dc3 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 0e8ff5cd23..5f3e1d5d69 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 162a3a4c27..1fc24300b2 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 4b6a996418..85bc267dd2 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 58231fa4ec..7be7d9f7db 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 0ff6670ee3..8598c601b6 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 a8ca1d7df1..6bb796a3a6 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 22ab6b66e7..c947ad9aa6 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 58bfc62333..5d61019b28 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 ef6b8484a6..f6a1492a8e 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 a1dd5ef0e8..19d83a8e46 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 fcaf3ad2f9..ac216644f5 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 f1f39f13871ea27bc4d9d66d77b1e571ef60a44d Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 24 Aug 2026 01:32:14 +0200 Subject: [PATCH 4/8] 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 ac216644f5..00089ab0d7 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 255069ad2630156628f76f624055436813ab7131 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 24 Aug 2026 03:16:36 +0200 Subject: [PATCH 5/8] 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 39d483a556..914db0609c 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 0000000000..30d86fe233 --- /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 0000000000..f0220ad99f --- /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 310b263b6d..2d2291f24a 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 85bc267dd2..4f266d8bbf 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 8598c601b6..05ea6d546f 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 f6a1492a8e..55e9ddc7bf 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 84201492b85db169aeb25b772a00daf2877470a5 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 24 Aug 2026 10:43:32 +0200 Subject: [PATCH 6/8] 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 dfec1651f4..8da1600dcf 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 df3f1b2dc3..4878de84dd 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 5f3e1d5d69..ed7236c3fe 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 1fc24300b2..aab398e193 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 d73b5fa38f..8dfbb3e397 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 85a2239267..19a90e4b79 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 4f266d8bbf..7b7af94afe 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 7be7d9f7db..9b6da780ce 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 05ea6d546f..6b3b25a55c 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 6bb796a3a6..9f688c1f47 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 c947ad9aa6..0686204fb3 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 5d61019b28..20714bdf83 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 55e9ddc7bf..15a1e3c4b9 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 19d83a8e46..6ef0484d9b 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 67ecb6ac87..34fdf59711 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 00089ab0d7..bbf0f46006 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 f3a65043a65068a5c9a2f631b536216c09019c4a Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Tue, 25 Aug 2026 03:44:18 +0200 Subject: [PATCH 7/8] 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 8dfbb3e397..503ab24428 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 19a90e4b79..e457002dad 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 34fdf59711..8d76abffef 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 bbf0f46006..7a60d4812b 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 From 1d243db4e4cc1f278348a1b41ae2eee256806ddb Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 18 Sep 2026 12:26:59 +0200 Subject: [PATCH 8/8] fix(cli): tighten retry suggestions and command guidance --- cli/cmd/compile.go | 1 - cli/cmd/dry_run.go | 11 ----- cli/cmd/exit_codes.go | 8 ---- cli/cmd/flag_alias.go | 7 ---- cli/cmd/project.go | 1 - cli/cmd/rerun_test.go | 29 ++++++++++---- cli/cmd/scan.go | 6 +-- cli/cmd/suggest.go | 87 ++++++++++++++++++++++++---------------- cli/cmd/test_init.go | 22 +++++----- cli/cmd/test_rule_run.go | 5 +-- docs/usage.md | 27 +++++++++++++ 11 files changed, 117 insertions(+), 87 deletions(-) diff --git a/cli/cmd/compile.go b/cli/cmd/compile.go index 8da1600dcf..b8596f18b1 100644 --- a/cli/cmd/compile.go +++ b/cli/cmd/compile.go @@ -110,7 +110,6 @@ Before your first compile, run "opentaint pull" one time. To scan the model, use }); err == nil { out.Blank() printCompileSummary(absOutputProjectModelPath) - out.Successf("Compilation completed.") suggest("To scan the compiled project model, run:", utils.BuildScanCommandFromCompile(projectRoot, absOutputProjectModelPath)) } else { out.InteractiveBlank() diff --git a/cli/cmd/dry_run.go b/cli/cmd/dry_run.go index 914db0609c..dd2edaa238 100644 --- a/cli/cmd/dry_run.go +++ b/cli/cmd/dry_run.go @@ -11,17 +11,11 @@ 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) 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:] { @@ -33,9 +27,6 @@ func rerunWithoutDryRun() string { return strings.Join(args, " ") } -// shellQuote single-quotes an argument that would break when copy-pasted into -// 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.ContainsFunc(arg, shellUnsafe) { return arg @@ -43,8 +34,6 @@ func shellQuote(arg string) string { 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': diff --git a/cli/cmd/exit_codes.go b/cli/cmd/exit_codes.go index 5de9a0912e..7f950d70c5 100644 --- a/cli/cmd/exit_codes.go +++ b/cli/cmd/exit_codes.go @@ -6,9 +6,6 @@ import ( "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} { @@ -17,17 +14,12 @@ func analyzerExitCodeRows() string { 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 + ` diff --git a/cli/cmd/flag_alias.go b/cli/cmd/flag_alias.go index 30d86fe233..d2d03d75dd 100644 --- a/cli/cmd/flag_alias.go +++ b/cli/cmd/flag_alias.go @@ -6,11 +6,6 @@ import ( "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 } @@ -31,8 +26,6 @@ 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) diff --git a/cli/cmd/project.go b/cli/cmd/project.go index ed7236c3fe..53b9229cf5 100644 --- a/cli/cmd/project.go +++ b/cli/cmd/project.go @@ -181,7 +181,6 @@ func (c *JavaAutobuilderConfig) printProjectSummary(config *project.Config) erro projectYamlPath := filepath.Join(c.outputDir, "project.yaml") c.logProjectSummary(projectYamlPath, config) - out.Successf("Project model generated.") suggest("To scan the generated model, run:", utils.BuildScanCommandFromCompile(c.outputDir, c.outputDir)) return nil } diff --git a/cli/cmd/rerun_test.go b/cli/cmd/rerun_test.go index 2d2291f24a..a57cd8aee5 100644 --- a/cli/cmd/rerun_test.go +++ b/cli/cmd/rerun_test.go @@ -3,6 +3,7 @@ package cmd import ( "os" "testing" + "time" "github.com/seqra/opentaint/internal/analyzer" ) @@ -48,6 +49,9 @@ func TestWithFlag(t *testing.T) { if got := withFlag("opentaint prune --yes", "--yes"); got != "opentaint prune --yes" { t.Fatalf("withFlag no-op = %q", got) } + if got := withFlag("opentaint scan path--debug", "--debug"); got != "opentaint scan path--debug --debug" { + t.Fatalf("withFlag substring match = %q", got) + } } func TestRerunReplacingFlagValueForm(t *testing.T) { @@ -79,11 +83,13 @@ func TestRerunReplacingFlagAppendsWhenAbsent(t *testing.T) { func TestDoubleMemory(t *testing.T) { cases := map[string]string{ - "8G": "16G", - "1024m": "2048m", - "83886080": "167772160", - "weird": "16G", - "": "16G", + "8G": "16G", + "12G": "16G", + "15G": "16G", + "1024m": "2048m", + "0G": "16G", + "weird": "16G", + "": "16G", } for in, want := range cases { if got := doubleMemory(in); got != want { @@ -100,14 +106,23 @@ func TestRetrySuggestion(t *testing.T) { t.Fatalf("OOM retry = %+v ok=%t", oom, ok) } - timeoutRetry, ok := retrySuggestion(analyzer.ExitTimeout, 900e9, "8G") + timeoutRetry, ok := retrySuggestion(analyzer.ExitTimeout, 10*time.Minute, "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" + want := "opentaint scan . --max-memory 8G --timeout 15m0s" if timeoutRetry.Command != want { t.Fatalf("timeout retry command = %q, want %q", timeoutRetry.Command, want) } + if _, ok := retrySuggestion(analyzer.ExitTimeout, 15*time.Minute, "8G"); ok { + t.Fatal("timeout at cap must not produce a retry suggestion") + } + if _, ok := retrySuggestion(analyzer.ExitOOM, 15*time.Minute, "16G"); ok { + t.Fatal("memory at cap must not produce a retry suggestion") + } + if retry, ok := retrySuggestion(analyzer.ExitTimeout, 10*time.Minute, "8G"); !ok || retry.Command != "opentaint scan . --max-memory 8G --timeout 15m0s" { + t.Fatalf("timeout cap retry = %+v ok=%t", retry, ok) + } if _, ok := retrySuggestion(analyzer.ExitException, 900e9, "8G"); ok { t.Fatal("exception exit code must not produce a retry suggestion") diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 7b7af94afe..79e8290d2f 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -500,8 +500,6 @@ 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, - // 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:", @@ -516,7 +514,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { Command: utils.NewSummaryCommand(absSarifReportPath).WithShowFindings().Build(), }) case analyzerFail == nil: - out.Successf("Scan completed. No vulnerabilities found at %s severity.", strings.Join(cfg.Severity, " or ")) + out.Successf("Scan completed. No %s findings were reported.", strings.Join(cfg.Severity, " or ")) if isDefaultSeverity(cfg.Severity) { suggestions = append(suggestions, output.Suggestion{ Description: "To also check note-level rules, run:", @@ -532,8 +530,6 @@ 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 != "" { diff --git a/cli/cmd/suggest.go b/cli/cmd/suggest.go index 416c76e694..70b9283c51 100644 --- a/cli/cmd/suggest.go +++ b/cli/cmd/suggest.go @@ -16,37 +16,47 @@ 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. +const ( + maxRetryMemory = 16 + maxRetryTimeout = 15 * time.Minute +) + func withFlag(command, flag string) string { - if strings.Contains(command, flag) { - return command + for _, token := range strings.Fields(command) { + if token == flag || strings.HasPrefix(token, 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: + if memoryGib(maxMemory) >= maxRetryMemory { + return output.Suggestion{}, false + } + next := doubleMemory(maxMemory) return output.Suggestion{ Description: "To retry with more memory, run:", - Command: rerunReplacingFlag(doubleMemory(maxMemory), "--max-memory"), + Command: rerunReplacingFlag(next, "--max-memory"), }, true case analyzer.ExitTimeout: + if timeout >= maxRetryTimeout { + return output.Suggestion{}, false + } + next := timeout * 2 + if next <= timeout || next > maxRetryTimeout { + next = maxRetryTimeout + } return output.Suggestion{ Description: "To retry with a longer timeout, run:", - Command: rerunReplacingFlag((timeout * 2).String(), "--timeout", "-t"), + Command: rerunReplacingFlag(next.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 @@ -76,8 +86,6 @@ func rerunReplacingFlag(value string, names ...string) string { 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' { @@ -88,15 +96,42 @@ func doubleMemory(value string) string { return "16G" } n, err := strconv.ParseInt(value[:digits], 10, 64) - if err != nil { + if err != nil || n <= 0 { + return "16G" + } + if memoryGib(value)*2 >= maxRetryMemory { 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. +func memoryGib(value string) int { + digits := 0 + for digits < len(value) && value[digits] >= '0' && value[digits] <= '9' { + digits++ + } + suffix := value[digits:] + if digits == 0 { + return 0 + } + n, err := strconv.ParseInt(value[:digits], 10, 64) + if err != nil || n <= 0 { + return 0 + } + switch suffix { + case "G", "g": + return int(n) + case "M", "m": + return int(n / 1024) + case "K", "k": + return int(n / (1024 * 1024)) + case "": + return int(n / (1024 * 1024 * 1024)) + default: + return 0 + } +} + func logSuggestion() (output.Suggestion, bool) { if globals.LogPath == "" { return output.Suggestion{}, false @@ -107,9 +142,6 @@ func logSuggestion() (output.Suggestion, bool) { }, true } -// appendLogSuggestion appends the log-file pointer to s when a log file is -// active and returns s unchanged otherwise. It centralizes the "lead with the -// log" idiom shared by buildFailSuggestions and the scan summary tail. func appendLogSuggestion(s []output.Suggestion) []output.Suggestion { if logSug, ok := logSuggestion(); ok { return append(s, logSug) @@ -117,29 +149,16 @@ func appendLogSuggestion(s []output.Suggestion) []output.Suggestion { return s } -// buildFailSuggestions returns a pointer to the log file (when one exists) -// followed by the contextual hints. The log pointer leads so the user always -// sees where to look for full details first. Building onto a fresh slice avoids -// mutating a caller's backing array when contextual is passed via the spread form. func buildFailSuggestions(contextual []output.Suggestion) []output.Suggestion { return append(appendLogSuggestion(nil), contextual...) } -// failWith prints an error message, renders a single Suggestions block leading -// with a pointer to the log file (when one exists) followed by any contextual -// hints, then exits the process with the given code. Use it for operational -// compile/scan failures; pure input/argument errors stay on out.Fatalf with -// their own usage hints. func failWith(code int, message string, contextual ...output.Suggestion) { out.Error(message) out.Suggestions(buildFailSuggestions(contextual)...) os.Exit(code) } -// failf formats an error message and fails with exit code 1 and no contextual -// suggestion. The log-file pointer is still added when a log file exists. Use -// it as the drop-in for bare out.Fatalf at operational compile/scan failure -// sites; see failWith. func failf(format string, args ...any) { failWith(1, fmt.Sprintf(format, args...)) } diff --git a/cli/cmd/test_init.go b/cli/cmd/test_init.go index 0686204fb3..0fdb63e666 100644 --- a/cli/cmd/test_init.go +++ b/cli/cmd/test_init.go @@ -60,13 +60,17 @@ Then compile the project with "opentaint compile" and run the samples with "open } 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 suggestions []output.Suggestion + for _, kind := range kinds { + dir := filepath.Join(args[0], kind) + modelDir := filepath.Join(dir, "model") + suggestions = append(suggestions, + output.Suggestion{Description: fmt.Sprintf("To add %s test samples, edit:", kind), Command: filepath.Join(dir, "rule-test.yaml")}, + output.Suggestion{Description: fmt.Sprintf("To compile the %s test project, run:", kind), Command: fmt.Sprintf("opentaint compile %s -o %s", shellQuote(dir), shellQuote(modelDir))}, + output.Suggestion{Description: fmt.Sprintf("To run the %s tests, run:", kind), Command: fmt.Sprintf("opentaint test rule run %s", shellQuote(modelDir))}, + ) + } + out.Suggestions(suggestions...) }, } @@ -103,8 +107,8 @@ Then compile the project with "opentaint compile" and run the samples with "open 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 --java-models ", modelDir)}, + output.Suggestion{Description: "To compile the test project, run:", Command: fmt.Sprintf("opentaint compile %s -o %s", shellQuote(dir), shellQuote(modelDir))}, + output.Suggestion{Description: "To run the tests, run:", Command: fmt.Sprintf("opentaint test approximation run %s --java-models ", shellQuote(modelDir))}, ) }, } diff --git a/cli/cmd/test_rule_run.go b/cli/cmd/test_rule_run.go index 15a1e3c4b9..136a138992 100644 --- a/cli/cmd/test_rule_run.go +++ b/cli/cmd/test_rule_run.go @@ -67,7 +67,7 @@ Compile the test project before you run the tests. To read the results, use "ope type testProjectOptions struct { label string - passedLine string // success status line, matching the documented exit-code 0 row + passedLine string tempDir string rulesets []string outputDir string @@ -168,9 +168,6 @@ 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. hint := output.Suggestion{ Description: "To stream the analyzer output, re-run with --debug:", Command: withFlag(rerunWithoutDryRun(), "--debug"), diff --git a/docs/usage.md b/docs/usage.md index 7a60d4812b..e487ae4441 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -117,6 +117,16 @@ 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) | +
+Deprecated flag names + +The old names remain accepted for compatibility and are deprecated: + +- `--passthrough-approximations` → `--passthrough-models` +- `--dataflow-approximations` → `--java-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 @@ -157,6 +167,9 @@ opentaint test rule reachability java/security/my-rule.yaml:my-rule --project-mo | `opentaint test rule run ` | Run detection-rule tests on a compiled project model | | `opentaint test rule reachability [source-path]` | Show why a rule does or does not fire | +Rule-test options include `--ruleset`, `--rule-id`, `--project-model`, `--entry-points`, `--output`, `--timeout`, `--max-memory`, and `--dry-run`. +Rule runs also accept `--java-models` and the deprecated `--dataflow-approximations` alias. Reachability accepts `--project-model` or a source path, but not both. + #### Approximation tests ```bash @@ -171,6 +184,8 @@ opentaint test approximation run .opentaint/test-compiled/my-approximation \ | `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 | +Approximation-test options include `--java-models`, `--output`, `--timeout`, `--max-memory`, and `--dry-run`. The deprecated `--dataflow-approximations` alias remains accepted. + Rule and approximation test runs write `test-result.json` and `test-results.sarif` to the selected output directory. ### opentaint compile @@ -232,6 +247,18 @@ opentaint scan --project-model ./project-model | `--dry-run` | Validate inputs and show what would run without generating the project model | | `--log-file` | Path to the log file (default: `/logs/.log`) | +### opentaint pull, update, and prune + +`opentaint pull` downloads the analyzer, autobuilder, built-in rules, and Java runtime. Use it before the first scan or to restore components removed by pruning. + +`opentaint update [version]` updates the OpenTaint binary. `--check` checks for an available update without installing it, and `--yes` skips confirmation. Homebrew and npm installations are updated with their package managers. + +`opentaint prune` removes old cached artifacts and models. By default it removes old artifacts, rules, JDK/JRE versions, and cached models. Use `--logs` or `--install` to include those categories, `--all` to select every category, `--dry-run` to preview deletion, and `--yes` to skip confirmation. `--all` cannot be combined with a category flag. + +### opentaint health + +`opentaint health` reports the autobuilder, analyzer, built-in rules, and Java runtime. Use `--autobuilder`, `--analyzer`, `--rules`, or `--runtime` to select one component. A selected missing component causes a non-zero exit status. + ## Model Caching When `opentaint scan` compiles a project, the resulting project model is cached in `~/.opentaint/cache/`. The cache directory name is derived from the project path (e.g. `my-project-a1b2c3d4`).