From 858de5f77ae344b2a36aab71ecb3c2bc7018acab Mon Sep 17 00:00:00 2001 From: Gr-i-niy Date: Mon, 2 Nov 2026 12:11:22 +0000 Subject: [PATCH 1/4] feat(cli): rename approximation commands and flags to models The commands are now model init, compile models, and test model init and run, and the flags are --dataflow-models and --passthrough-models. The old names stay as hidden aliases that print a deprecation notice and run the new command or set the new flag. --- cli/cmd/analyzer_inputs.go | 29 +++++-- cli/cmd/approximation.go | 46 ++++++----- cli/cmd/compile_approximations.go | 32 ++++---- cli/cmd/compile_approximations_cmd.go | 37 ++++----- cli/cmd/deprecated.go | 37 +++++++++ cli/cmd/deprecated_test.go | 80 +++++++++++++++++++ cli/cmd/scan.go | 16 ++-- cli/cmd/test.go | 18 +++-- cli/cmd/test_approximation_run.go | 33 ++++---- cli/cmd/test_init.go | 23 +++--- cli/cmd/test_rule_run.go | 14 ++-- .../approximation/approximation_test.go | 2 +- cli/internal/approximation/descriptor.go | 2 +- cli/internal/approximation/resolve.go | 4 +- .../utils/opentaint_command_builder.go | 8 +- 15 files changed, 265 insertions(+), 116 deletions(-) create mode 100644 cli/cmd/deprecated.go create mode 100644 cli/cmd/deprecated_test.go diff --git a/cli/cmd/analyzer_inputs.go b/cli/cmd/analyzer_inputs.go index d9fabfe8b4..cb49c8a8f1 100644 --- a/cli/cmd/analyzer_inputs.go +++ b/cli/cmd/analyzer_inputs.go @@ -3,31 +3,44 @@ package cmd import ( "github.com/seqra/opentaint/internal/approximation" "github.com/seqra/opentaint/internal/utils/log" + "github.com/spf13/cobra" ) -// addDataflowApproximations resolves the paths to compiled model directories. +func addDataflowModelsFlag(cmd *cobra.Command, paths *[]string) { + cmd.Flags().StringArrayVar(paths, "dataflow-models", nil, + "Dataflow model project, build output, or class directory (repeatable)") + addDeprecatedFlagAlias(cmd.Flags(), "dataflow-models", "dataflow-approximations") +} + +func addPassthroughModelsFlag(cmd *cobra.Command, paths *[]string) { + cmd.Flags().StringArrayVar(paths, "passthrough-models", nil, + "Pass-through model YAML file or directory (repeatable)") + addDeprecatedFlagAlias(cmd.Flags(), "passthrough-models", "passthrough-approximations") +} + +// addDataflowModels resolves the paths to compiled model directories. // It rebuilds a model project when its inputs change. -func addDataflowApproximations(b *AnalyzerBuilder, paths []string, analyzerJarPath string) { +func addDataflowModels(b *AnalyzerBuilder, paths []string, analyzerJarPath string) { if len(paths) == 0 { return } absPaths := make([]string, 0, len(paths)) - for _, approxPath := range paths { - absPaths = append(absPaths, log.AbsPathOrExit(approxPath, "dataflow-approximations")) + for _, modelPath := range paths { + absPaths = append(absPaths, log.AbsPathOrExit(modelPath, "dataflow-models")) } - classDirs, err := approximation.ResolveAll(absPaths, newApproximationBuilder(analyzerJarPath)) + classDirs, err := approximation.ResolveAll(absPaths, newModelBuilder(analyzerJarPath)) if err != nil { - out.Fatalf("Failed to apply dataflow approximations: %s", err) + out.Fatalf("Failed to apply dataflow models: %s", err) } for _, classDir := range classDirs { b.AddDataflowApproximations(classDir) } } -func addPassthroughApproximations(b *AnalyzerBuilder, paths []string) { +func addPassthroughModels(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/approximation.go b/cli/cmd/approximation.go index 61d5050c88..ca289348db 100644 --- a/cli/cmd/approximation.go +++ b/cli/cmd/approximation.go @@ -8,23 +8,23 @@ import ( "github.com/spf13/cobra" ) -var initApproximationDeps []string +var initModelDeps []string -var approximationCmd = &cobra.Command{ - Use: "approximation", - Short: "Create dataflow approximation projects", - Long: `A dataflow approximation project holds code-based models of how taint moves through +var modelCmd = &cobra.Command{ + Use: "model", + Short: "Create dataflow model projects", + Long: `A dataflow model project holds code-based models of how taint moves through methods the analyzer cannot see into, together with the exact dependency versions those models are written against. -Compile one with 'opentaint compile approximations', and apply it with the ---dataflow-approximations flag of 'opentaint scan' or 'opentaint test approximation run'.`, +Compile one with 'opentaint compile models', and apply it with the +--dataflow-models flag of 'opentaint scan' or 'opentaint test model run'.`, } -var approximationInitCmd = &cobra.Command{ +var modelInitCmd = &cobra.Command{ Use: "init ", - Short: "Create a dataflow approximation project", - Long: `Create a Gradle project for OpenTaint dataflow approximation models. + Short: "Create a dataflow model project", + Long: `Create a Gradle project for OpenTaint dataflow models. The project includes: - build.gradle.kts with compile-only dependencies, settings.gradle.kts @@ -48,27 +48,35 @@ nothing about the project under analysis, decide what the models compile against // Warn the user because this removes dependency pins that they added manually. existing := approximation.IsProject(projectPath) if existing { - out.Warnf("Replacing the build file of the existing approximation project at %s; "+ + out.Warnf("Replacing the build file of the existing model project at %s; "+ "pass every dependency the models need, not only the new ones", projectPath) } - if err := approximation.Scaffold(projectPath, initApproximationDeps, analyzerJarPath); err != nil { - out.Fatalf("Failed to create approximation project: %s", err) + if err := approximation.Scaffold(projectPath, initModelDeps, analyzerJarPath); err != nil { + out.Fatalf("Failed to create model project: %s", err) } if existing { - fmt.Printf("Approximation project updated at %s\n", projectPath) + fmt.Printf("Model project updated at %s\n", projectPath) } else { - fmt.Printf("Approximation project initialized at %s\n", projectPath) + fmt.Printf("Model project initialized at %s\n", projectPath) } - suggest("To compile the models run", fmt.Sprintf("opentaint compile approximations %s", args[0])) + suggest("To compile the models run", fmt.Sprintf("opentaint compile models %s", args[0])) }, } func init() { - rootCmd.AddCommand(approximationCmd) - approximationCmd.AddCommand(approximationInitCmd) + rootCmd.AddCommand(modelCmd) + modelCmd.AddCommand(modelInitCmd) - approximationInitCmd.Flags().StringArrayVar(&initApproximationDeps, "dependency", nil, + modelInitCmd.Flags().StringArrayVar(&initModelDeps, "dependency", nil, "Compile-only Maven dependency coordinates the models are written against (repeatable)") + + approximationCmd := &cobra.Command{ + Use: "approximation", + Short: modelCmd.Short, + Deprecated: "use 'opentaint model' instead", + } + approximationCmd.AddCommand(deprecatedCommand(modelInitCmd, "init", "opentaint model init")) + rootCmd.AddCommand(approximationCmd) } diff --git a/cli/cmd/compile_approximations.go b/cli/cmd/compile_approximations.go index 52b08ba73f..923c65cb98 100644 --- a/cli/cmd/compile_approximations.go +++ b/cli/cmd/compile_approximations.go @@ -11,21 +11,21 @@ import ( "github.com/seqra/opentaint/internal/output" ) -// approximationBuilder compiles a model project with the autobuilder. +// modelBuilder compiles a model project with the autobuilder. // The model project supplies the compile dependencies. -type approximationBuilder struct { +type modelBuilder struct { analyzerJarPath string apiJar []byte } -func newApproximationBuilder(analyzerJarPath string) approximation.Builder { - return &approximationBuilder{analyzerJarPath: analyzerJarPath} +func newModelBuilder(analyzerJarPath string) approximation.Builder { + return &modelBuilder{analyzerJarPath: analyzerJarPath} } // Fingerprint identifies the compiler a build was produced with. // The source stamp covers the model project but not the tool that reads it, so without // this an upgraded CLI keeps serving models compiled by the previous one. -func (b *approximationBuilder) Fingerprint() (string, error) { +func (b *modelBuilder) Fingerprint() (string, error) { autobuilder := globals.ArtifactByKind("autobuilder") if autobuilder.Override == "" { return "autobuilder " + autobuilder.Version, nil @@ -40,9 +40,9 @@ func (b *approximationBuilder) Fingerprint() (string, error) { autobuilder.Override, info.Size(), info.ModTime().UnixNano()), nil } -// Prepare updates the approximation API jar in the model project. +// Prepare updates the model API jar in the model project. // The builder reads the jar from the analyzer only once. -func (b *approximationBuilder) Prepare(projectDir string) error { +func (b *modelBuilder) Prepare(projectDir string) error { if b.apiJar == nil { apiJar, err := approximation.ExtractApiJar(b.analyzerJarPath) if err != nil { @@ -53,7 +53,7 @@ func (b *approximationBuilder) Prepare(projectDir string) error { return approximation.WriteApiJar(projectDir, b.apiJar) } -func (b *approximationBuilder) Compile(projectDir, projectModelDir string) error { +func (b *modelBuilder) Compile(projectDir, projectModelDir string) error { autobuilderJarPath, err := ensureAutobuilderAvailable() if err != nil { return fmt.Errorf("failed to resolve autobuilder: %w", err) @@ -61,27 +61,27 @@ func (b *approximationBuilder) Compile(projectDir, projectModelDir string) error javaRunner := newAutobuilderJavaRunner() if _, err := javaRunner.EnsureJava(); err != nil { - return fmt.Errorf("failed to resolve Java for approximation compilation: %w", err) + return fmt.Errorf("failed to resolve Java for model compilation: %w", err) } - logsDir, err := os.MkdirTemp("", "opentaint-approximation-logs-*") + logsDir, err := os.MkdirTemp("", "opentaint-model-logs-*") if err != nil { - return fmt.Errorf("failed to create temp directory for approximation build logs: %w", err) + return fmt.Errorf("failed to create temp directory for model build logs: %w", err) } - logsFile := filepath.Join(logsDir, "approximation-build.log") + logsFile := filepath.Join(logsDir, "model-build.log") // Show the spinner for direct and automatic builds. - output.LogInfof("Compiling approximation project %s", projectDir) + output.LogInfof("Compiling model project %s", projectDir) compile := func() error { return compileProjectWithLogs(projectModelDir, projectDir, autobuilderJarPath, logsFile, true, javaRunner) } - if err := out.RunWithSpinner("Compiling approximation project "+filepath.Base(projectDir), compile); err != nil { + if err := out.RunWithSpinner("Compiling model project "+filepath.Base(projectDir), compile); err != nil { // Show compiler errors when they are available. // Keep the full log if no compiler error is available. if diagnostics := autobuilder.CompilerDiagnostics(logsFile); diagnostics != "" { - return fmt.Errorf("approximation project %s failed to compile:\n%s", projectDir, diagnostics) + return fmt.Errorf("model project %s failed to compile:\n%s", projectDir, diagnostics) } - return fmt.Errorf("failed to compile approximation project %s (build log: %s): %w", projectDir, logsFile, err) + return fmt.Errorf("failed to compile model project %s (build log: %s): %w", projectDir, logsFile, err) } _ = os.RemoveAll(logsDir) diff --git a/cli/cmd/compile_approximations_cmd.go b/cli/cmd/compile_approximations_cmd.go index d9ab54bcaf..5a2ebe77fa 100644 --- a/cli/cmd/compile_approximations_cmd.go +++ b/cli/cmd/compile_approximations_cmd.go @@ -8,31 +8,31 @@ import ( "github.com/spf13/cobra" ) -var compileApproximationsOutput string +var compileModelsOutput string -var compileApproximationsCmd = &cobra.Command{ - Use: "approximations ", - Short: "Compile a dataflow approximation project", - Long: `Compile the dataflow approximation models in against the +var compileModelsCmd = &cobra.Command{ + Use: "models ", + Short: "Compile a dataflow model project", + Long: `Compile the dataflow models in against the dependencies that project pins in its own build file. This is the model project, not the project to scan: its build.gradle.kts lists the exact library versions the models are written against, so the same models compile identically wherever they are applied. -Scanning and testing build an approximation project on demand, so running this is only +Scanning and testing build a model project on demand, so running this is only needed to compile ahead of time or to see compilation errors on their own.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - projectPath := log.AbsPathOrExit(args[0], "approximation-project") + projectPath := log.AbsPathOrExit(args[0], "model-project") if !approximation.IsProject(projectPath) { - out.Fatalf("%s is not an approximation project: it has no build.gradle.kts declaring the "+ - "dependencies its models are written against.\nCreate one with 'opentaint approximation init %s "+ + out.Fatalf("%s is not a model project: it has no build.gradle.kts declaring the "+ + "dependencies its models are written against.\nCreate one with 'opentaint model init %s "+ "--dependency '", projectPath, args[0]) } - outputPath := compileApproximationsOutput + outputPath := compileModelsOutput if outputPath == "" { outputPath = approximation.BuildDir(projectPath) } else { @@ -44,22 +44,23 @@ needed to compile ahead of time or to see compilation errors on their own.`, out.Fatalf("Failed to resolve analyzer: %s", err) } - if err := approximation.Build(projectPath, outputPath, newApproximationBuilder(analyzerJarPath)); err != nil { - out.Fatalf("Approximation compilation failed: %s", err) + if err := approximation.Build(projectPath, outputPath, newModelBuilder(analyzerJarPath)); err != nil { + out.Fatalf("Model compilation failed: %s", err) } - out.Section("Approximation Compile Summary"). - Field("Approximation project", projectPath). + out.Section("Model Compile Summary"). + Field("Model project", projectPath). Field("Compiled models written to", approximation.ClassesDir(outputPath)). Render() suggest("To apply these models in a scan run", - fmt.Sprintf("opentaint scan --dataflow-approximations %s", args[0])) + fmt.Sprintf("opentaint scan --dataflow-models %s", args[0])) }, } func init() { - compileCmd.AddCommand(compileApproximationsCmd) + compileCmd.AddCommand(compileModelsCmd) - compileApproximationsCmd.Flags().StringVarP(&compileApproximationsOutput, "output", "o", "", - "Path to the compiled models (default: /.opentaint/build)") + compileModelsCmd.Flags().StringVarP(&compileModelsOutput, "output", "o", "", + "Path to the compiled models (default: /.opentaint/build)") + compileCmd.AddCommand(deprecatedCommand(compileModelsCmd, "approximations", "opentaint compile models")) } diff --git a/cli/cmd/deprecated.go b/cli/cmd/deprecated.go new file mode 100644 index 0000000000..130ceacaa1 --- /dev/null +++ b/cli/cmd/deprecated.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// deprecatedCommand returns a hidden copy of cmd under its old name. Running it prints that the +// name is deprecated, pointing at newPath, and then runs cmd with the same flags. +func deprecatedCommand(cmd *cobra.Command, oldName, newPath string) *cobra.Command { + alias := &cobra.Command{ + Use: oldName + strings.TrimPrefix(cmd.Use, cmd.Name()), + Short: cmd.Short, + Long: cmd.Long, + Args: cmd.Args, + Run: cmd.Run, + Annotations: cmd.Annotations, + Deprecated: "use '" + newPath + "' instead", + } + alias.Flags().AddFlagSet(cmd.Flags()) + return alias +} + +// addDeprecatedFlagAlias registers oldName as a hidden, deprecated name of the flag newName. +// Both names set the same value, so they can be mixed on one command line. +func addDeprecatedFlagAlias(flags *pflag.FlagSet, newName, oldName string) { + flag := flags.Lookup(newName) + flags.AddFlag(&pflag.Flag{ + Name: oldName, + Usage: flag.Usage, + Value: flag.Value, + DefValue: flag.DefValue, + }) + _ = flags.MarkDeprecated(oldName, "use --"+newName+" instead") +} diff --git a/cli/cmd/deprecated_test.go b/cli/cmd/deprecated_test.go new file mode 100644 index 0000000000..89dd128eaa --- /dev/null +++ b/cli/cmd/deprecated_test.go @@ -0,0 +1,80 @@ +package cmd + +import ( + "reflect" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestDeprecatedCommandsPointAtTheirNewNames(t *testing.T) { + for _, tc := range []struct { + old []string + newPath string + flag string + }{ + {[]string{"approximation", "init"}, "opentaint model init", "dependency"}, + {[]string{"compile", "approximations"}, "opentaint compile models", "output"}, + {[]string{"test", "approximation", "init"}, "opentaint test model init", "dependency"}, + {[]string{"test", "approximation", "run"}, "opentaint test model run", "dataflow-models"}, + } { + t.Run(strings.Join(tc.old, " "), func(t *testing.T) { + command, rest, err := rootCmd.Find(tc.old) + if err != nil || len(rest) != 0 || command.Name() != tc.old[len(tc.old)-1] { + t.Fatalf("Find(%v) = %s %v %v", tc.old, command.CommandPath(), rest, err) + } + if !strings.Contains(command.Deprecated, tc.newPath) { + t.Errorf("Deprecated = %q, want it to name %q", command.Deprecated, tc.newPath) + } + if command.IsAvailableCommand() { + t.Error("a deprecated command is listed in help") + } + if command.Run == nil || command.Flags().Lookup(tc.flag) == nil { + t.Errorf("the alias does not run the new command with its flags") + } + + current, _, err := rootCmd.Find(strings.Fields(strings.TrimPrefix(tc.newPath, "opentaint "))) + if err != nil || current.Deprecated != "" || !current.IsAvailableCommand() { + t.Errorf("new command %s is missing or hidden: %v", tc.newPath, err) + } + }) + } +} + +func TestDeprecatedFlagAliasesFeedTheNewFlag(t *testing.T) { + var dataflow, passthrough []string + command := &cobra.Command{Use: "probe", Run: func(*cobra.Command, []string) {}} + addDataflowModelsFlag(command, &dataflow) + addPassthroughModelsFlag(command, &passthrough) + + err := command.ParseFlags([]string{ + "--dataflow-models", "a", "--dataflow-approximations", "b", + "--passthrough-approximations", "c", "--passthrough-models", "d", + }) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(dataflow, []string{"a", "b"}) { + t.Errorf("dataflow models = %v, want [a b]", dataflow) + } + if !reflect.DeepEqual(passthrough, []string{"c", "d"}) { + t.Errorf("passthrough models = %v, want [c d]", passthrough) + } + for _, old := range []string{"dataflow-approximations", "passthrough-approximations"} { + flag := command.Flags().Lookup(old) + if !flag.Hidden || !strings.Contains(flag.Deprecated, "-models") { + t.Errorf("--%s: hidden=%v deprecated=%q, want a hidden alias naming the new flag", old, flag.Hidden, flag.Deprecated) + } + } +} + +func TestModelFlagsOnAnalyzerCommands(t *testing.T) { + for _, command := range []*cobra.Command{scanCmd, testRuleRunCmd} { + for _, name := range []string{"dataflow-models", "dataflow-approximations", "passthrough-models", "passthrough-approximations"} { + if command.Flags().Lookup(name) == nil { + t.Errorf("%s has no --%s", command.CommandPath(), name) + } + } + } +} diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 30f51ed2fa..33c94898ab 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -34,8 +34,8 @@ type ScanConfig struct { Recompile bool LogFile string RuleID []string - PassthroughApproximations []string - DataflowApproximations []string + PassthroughModels []string + DataflowModels []string TrackExternalMethods bool IterationLimit int @@ -142,9 +142,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-approximations", nil, "Pass-through approximation YAML file or directory (repeatable)") + addPassthroughModelsFlag(cmd, &scanFlags.PassthroughModels) - cmd.Flags().StringArrayVar(&scanFlags.DataflowApproximations, "dataflow-approximations", nil, "Dataflow approximation project, build output, or class directory (repeatable)") + addDataflowModelsFlag(cmd, &scanFlags.DataflowModels) cmd.Flags().BoolVar(&scanFlags.TrackExternalMethods, "track-external-methods", false, "Write external-method coverage files next to the SARIF report") @@ -159,8 +159,8 @@ func currentScanBuilder(cfg ScanConfig, sourcePath string) *utils.OpentaintComma WithRuleset(cfg.Ruleset). WithSemgrepCompatibility(cfg.SemgrepCompatibilitySarif). WithRuleID(cfg.RuleID). - WithPassthroughApproximations(cfg.PassthroughApproximations). - WithDataflowApproximations(cfg.DataflowApproximations). + WithPassthroughModels(cfg.PassthroughModels). + WithDataflowModels(cfg.DataflowModels). WithTrackExternalMethods(cfg.TrackExternalMethods). WithIterationLimit(cfg.IterationLimit) if !isDefaultSeverity(cfg.Severity) { @@ -388,7 +388,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { for _, ruleID := range ruleIDs { nativeBuilder.AddRuleID(ruleID) } - addPassthroughApproximations(nativeBuilder, cfg.PassthroughApproximations) + addPassthroughModels(nativeBuilder, cfg.PassthroughModels) if cfg.TrackExternalMethods { nativeBuilder.SetTrackExternalMethods(true) } @@ -408,7 +408,7 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { } nativeBuilder.SetJarPath(analyzerJarPath) - addDataflowApproximations(nativeBuilder, cfg.DataflowApproximations, analyzerJarPath) + addDataflowModels(nativeBuilder, cfg.DataflowModels, analyzerJarPath) analyzerJavaRunner := newAnalyzerJavaRunner() if _, err := analyzerJavaRunner.EnsureJava(); err != nil { diff --git a/cli/cmd/test.go b/cli/cmd/test.go index a31a3cc73b..eaca915602 100644 --- a/cli/cmd/test.go +++ b/cli/cmd/test.go @@ -8,8 +8,8 @@ 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.`, + Short: "Create and run rule and model tests", + Long: `Tools for creating test projects, running annotated rule and model tests, and debugging rule reachability.`, } var testRuleCmd = &cobra.Command{ @@ -17,14 +17,22 @@ var testRuleCmd = &cobra.Command{ Short: "Create, run, and debug detection-rule tests", } +var testModelCmd = &cobra.Command{ + Use: "model", + Short: "Create and run dataflow model tests", +} + +// testApproximationCmd is the deprecated name of testModelCmd. var testApproximationCmd = &cobra.Command{ - Use: "approximation", - Short: "Create and run dataflow-approximation tests", + Use: "approximation", + Short: testModelCmd.Short, + Deprecated: "use 'opentaint test model' instead", } func init() { rootCmd.AddCommand(testCmd) testCmd.AddCommand(testRuleCmd) + testCmd.AddCommand(testModelCmd) testCmd.AddCommand(testApproximationCmd) } @@ -43,5 +51,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, "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 project, build output, or class directory (repeatable)") + addDataflowModelsFlag(cmd, dataflow) } diff --git a/cli/cmd/test_approximation_run.go b/cli/cmd/test_approximation_run.go index 018e16c03d..0e81b29f7f 100644 --- a/cli/cmd/test_approximation_run.go +++ b/cli/cmd/test_approximation_run.go @@ -9,21 +9,21 @@ import ( ) var ( - testApproxOutputDir string - testApproxTimeout time.Duration - testApproxMaxMemory string - testApproxDataflow []string + testModelOutputDir string + testModelTimeout time.Duration + testModelMaxMemory string + testModelDataflow []string ) -var testApproximationRunCmd = &cobra.Command{ +var testModelRunCmd = &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 model tests on a compiled project model", + Long: `Run the samples specified in rule-test.yaml with the supplied dataflow models applied. A built-in source-to-sink harness rule is applied automatically; positive samples reference the approximation-rule.yaml rule with id "approximation-rule". -` + testExitCodesHelp("All approximation tests passed"), +` + testExitCodesHelp("All model tests passed"), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { ruleDir, err := os.MkdirTemp("", "opentaint-approx-rule-*") @@ -35,18 +35,19 @@ approximation-rule.yaml rule with id "approximation-rule". } runTestProject(args[0], testProjectOptions{ - label: "Approximation tests", - tempDir: "opentaint-test-approximations-*", + label: "Model tests", + tempDir: "opentaint-test-models-*", rulesets: []string{ruleDir}, - outputDir: testApproxOutputDir, - timeout: testApproxTimeout, - maxMemory: testApproxMaxMemory, - dataflowApprox: testApproxDataflow, + outputDir: testModelOutputDir, + timeout: testModelTimeout, + maxMemory: testModelMaxMemory, + dataflowModels: testModelDataflow, }) }, } func init() { - testApproximationCmd.AddCommand(testApproximationRunCmd) - addTestRunFlags(testApproximationRunCmd, &testApproxOutputDir, &testApproxTimeout, &testApproxMaxMemory, &testApproxDataflow) + testModelCmd.AddCommand(testModelRunCmd) + addTestRunFlags(testModelRunCmd, &testModelOutputDir, &testModelTimeout, &testModelMaxMemory, &testModelDataflow) + testApproximationCmd.AddCommand(deprecatedCommand(testModelRunCmd, "run", "opentaint test model run")) } diff --git a/cli/cmd/test_init.go b/cli/cmd/test_init.go index 2c4966f822..c7aab49d91 100644 --- a/cli/cmd/test_init.go +++ b/cli/cmd/test_init.go @@ -11,7 +11,7 @@ import ( ) var initRuleProjectDeps []string -var initApproxProjectDeps []string +var initModelProjectDeps []string var initRuleSinksOnly bool var initRuleSourcesOnly bool @@ -55,10 +55,10 @@ Use --dependency to add compile-only Maven dependencies for the samples.`, }, } -var testApproximationInitCmd = &cobra.Command{ +var testModelInitCmd = &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 model test project", + Long: `Create a minimal Gradle project for testing OpenTaint dataflow models. The project includes: - build.gradle.kts with compile-only dependencies @@ -68,19 +68,19 @@ The project includes: Positive and negative samples are specified via rule-test.yaml. -The approximation under test is supplied separately at test time with ---dataflow-approximations. +The model under test is supplied separately at test time with +--dataflow-models. Use --dependency to add compile-only Maven dependencies for the samples.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - if err := testproject.Bootstrap(args[0], "approximation-test-project", initApproxProjectDeps); err != nil { + if err := testproject.Bootstrap(args[0], "approximation-test-project", initModelProjectDeps); err != nil { out.Fatalf("Failed to bootstrap test project: %s", err) } if err := testapprox.Scaffold(args[0]); err != nil { - out.Fatalf("Failed to scaffold approximation project: %s", err) + out.Fatalf("Failed to scaffold model test project: %s", err) } - fmt.Printf("Approximation test project initialized at %s\n", args[0]) + fmt.Printf("Model test project initialized at %s\n", args[0]) }, } @@ -93,7 +93,8 @@ func init() { testRuleInitCmd.Flags().BoolVar(&initRuleSourcesOnly, "sources-only", false, "Create only the sources test project") - testApproximationCmd.AddCommand(testApproximationInitCmd) - testApproximationInitCmd.Flags().StringArrayVar(&initApproxProjectDeps, "dependency", nil, + testModelCmd.AddCommand(testModelInitCmd) + testModelInitCmd.Flags().StringArrayVar(&initModelProjectDeps, "dependency", nil, "Compile-only Maven dependency coordinates for generated samples (repeatable)") + testApproximationCmd.AddCommand(deprecatedCommand(testModelInitCmd, "init", "opentaint test model init")) } diff --git a/cli/cmd/test_rule_run.go b/cli/cmd/test_rule_run.go index 6c7261e0fa..cb7575d571 100644 --- a/cli/cmd/test_rule_run.go +++ b/cli/cmd/test_rule_run.go @@ -39,8 +39,8 @@ compiled project model. timeout: testRulesTimeout, maxMemory: testRulesMaxMemory, ruleIDs: testRulesRuleID, - dataflowApprox: testRulesDataflow, - passthroughApprox: testRulesPassthrough, + dataflowModels: testRulesDataflow, + passthroughModels: testRulesPassthrough, includeBuiltinRules: true, }) }, @@ -54,8 +54,8 @@ type testProjectOptions struct { timeout time.Duration maxMemory string ruleIDs []string - dataflowApprox []string - passthroughApprox []string + dataflowModels []string + passthroughModels []string includeBuiltinRules bool } @@ -128,8 +128,8 @@ func runTestProject(projectModelArg string, opts testProjectOptions) { } builder.SetJarPath(analyzerJarPath) - addDataflowApproximations(builder, opts.dataflowApprox, analyzerJarPath) - addPassthroughApproximations(builder, opts.passthroughApprox) + addDataflowModels(builder, opts.dataflowModels, analyzerJarPath) + addPassthroughModels(builder, opts.passthroughModels) javaRunner := newAnalyzerJavaRunner() if _, err := javaRunner.EnsureJava(); err != nil { @@ -173,5 +173,5 @@ func init() { testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleset, "ruleset", nil, "Ruleset file or directory to test (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)") + addPassthroughModelsFlag(testRuleRunCmd, &testRulesPassthrough) } diff --git a/cli/internal/approximation/approximation_test.go b/cli/internal/approximation/approximation_test.go index 619f83333d..a8122791b9 100644 --- a/cli/internal/approximation/approximation_test.go +++ b/cli/internal/approximation/approximation_test.go @@ -361,7 +361,7 @@ func TestResolveLooseSourcesReportsMigration(t *testing.T) { if err == nil { t.Fatal("expected loose .java sources to be rejected") } - if !strings.Contains(err.Error(), "approximation init") { + if !strings.Contains(err.Error(), "opentaint model init") { t.Errorf("error should point at the migration command, got: %v", err) } } diff --git a/cli/internal/approximation/descriptor.go b/cli/internal/approximation/descriptor.go index cb118783ce..1fd66b4046 100644 --- a/cli/internal/approximation/descriptor.go +++ b/cli/internal/approximation/descriptor.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v2" ) -const descriptorHeader = "# Generated by 'opentaint compile approximations'. Do not edit.\n" +const descriptorHeader = "# Generated by 'opentaint compile models'. Do not edit.\n" // descriptor identifies the source and dependencies of a model build. type descriptor struct { diff --git a/cli/internal/approximation/resolve.go b/cli/internal/approximation/resolve.go index c0505e3474..168ac2eba2 100644 --- a/cli/internal/approximation/resolve.go +++ b/cli/internal/approximation/resolve.go @@ -147,7 +147,7 @@ func ambiguousDirError(dir, classFile string) error { "%s holds compiled classes of its own (e.g. %s) and an approximation project or build "+ "output below it, so it is neither a class directory nor a directory of them.\n"+ "Move the classes into a directory of their own, or pass each approximation path to "+ - "--dataflow-approximations separately", + "--dataflow-models separately", dir, classFile) } @@ -157,7 +157,7 @@ func emptyPathError(path string) error { return fmt.Errorf( "%s holds approximation sources (e.g. %s) but no approximation project: nothing declares "+ "the dependencies to compile the models against.\n"+ - "Create a project per model directory with 'opentaint approximation init "+ + "Create a project per model directory with 'opentaint model init "+ "--dependency ' and move its sources under /src/main/java", path, source, ) diff --git a/cli/internal/utils/opentaint_command_builder.go b/cli/internal/utils/opentaint_command_builder.go index 1eb81a09c6..8fc543424e 100644 --- a/cli/internal/utils/opentaint_command_builder.go +++ b/cli/internal/utils/opentaint_command_builder.go @@ -181,19 +181,19 @@ func (cb *OpentaintCommandBuilder) WithRuleID(ruleIDs []string) *OpentaintComman return cb } -func (cb *OpentaintCommandBuilder) WithPassthroughApproximations(paths []string) *OpentaintCommandBuilder { +func (cb *OpentaintCommandBuilder) WithPassthroughModels(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 } -func (cb *OpentaintCommandBuilder) WithDataflowApproximations(paths []string) *OpentaintCommandBuilder { +func (cb *OpentaintCommandBuilder) WithDataflowModels(paths []string) *OpentaintCommandBuilder { for _, p := range paths { if p != "" { - cb.arrayFlags["dataflow-approximations"] = append(cb.arrayFlags["dataflow-approximations"], p) + cb.arrayFlags["dataflow-models"] = append(cb.arrayFlags["dataflow-models"], p) } } return cb From 4bbf43685486df14c78d6f957e218da19bb94ebd Mon Sep 17 00:00:00 2001 From: Gr-i-niy Date: Mon, 2 Nov 2026 12:12:28 +0000 Subject: [PATCH 2/4] refactor(cli): use model names for packages, files, and the harness rule The packages are now internal/models and internal/testmodel, and the command files follow the command names. The harness rule of model tests is model-rule.yaml#model-rule, the test project is model-test-project, and the Gradle project of a model project is dependency-models. Messages say model instead of approximation. The approximation.yaml descriptor and the approximations API jar keep their names. --- cli/cmd/analyzer_inputs.go | 4 ++-- ...le_approximations.go => compile_models.go} | 8 ++++---- ...ximations_cmd.go => compile_models_cmd.go} | 10 +++++----- cli/cmd/{approximation.go => model.go} | 6 +++--- cli/cmd/test_init.go | 8 ++++---- ...approximation_run.go => test_model_run.go} | 8 ++++---- .../{approximation => models}/build.go | 16 +++++++-------- .../{approximation => models}/descriptor.go | 4 ++-- .../models_test.go} | 6 +++--- .../{approximation => models}/project.go | 4 ++-- .../{approximation => models}/resolve.go | 20 +++++++++---------- .../{approximation => models}/scaffold.go | 8 ++++---- .../{approximation => models}/stamp.go | 4 ++-- .../example/model-rule.yaml} | 6 +++--- .../example/src/main/java/test/Taint.java | 0 .../testapprox.go => testmodel/testmodel.go} | 8 ++++---- 16 files changed, 60 insertions(+), 60 deletions(-) rename cli/cmd/{compile_approximations.go => compile_models.go} (92%) rename cli/cmd/{compile_approximations_cmd.go => compile_models_cmd.go} (85%) rename cli/cmd/{approximation.go => model.go} (93%) rename cli/cmd/{test_approximation_run.go => test_model_run.go} (85%) rename cli/internal/{approximation => models}/build.go (93%) rename cli/internal/{approximation => models}/descriptor.go (92%) rename cli/internal/{approximation/approximation_test.go => models/models_test.go} (99%) rename cli/internal/{approximation => models}/project.go (98%) rename cli/internal/{approximation => models}/resolve.go (87%) rename cli/internal/{approximation => models}/scaffold.go (90%) rename cli/internal/{approximation => models}/stamp.go (95%) rename cli/internal/{testapprox/example/approximation-rule.yaml => testmodel/example/model-rule.yaml} (72%) rename cli/internal/{testapprox => testmodel}/example/src/main/java/test/Taint.java (100%) rename cli/internal/{testapprox/testapprox.go => testmodel/testmodel.go} (77%) diff --git a/cli/cmd/analyzer_inputs.go b/cli/cmd/analyzer_inputs.go index cb49c8a8f1..2d7da3a483 100644 --- a/cli/cmd/analyzer_inputs.go +++ b/cli/cmd/analyzer_inputs.go @@ -1,7 +1,7 @@ package cmd import ( - "github.com/seqra/opentaint/internal/approximation" + "github.com/seqra/opentaint/internal/models" "github.com/seqra/opentaint/internal/utils/log" "github.com/spf13/cobra" ) @@ -30,7 +30,7 @@ func addDataflowModels(b *AnalyzerBuilder, paths []string, analyzerJarPath strin absPaths = append(absPaths, log.AbsPathOrExit(modelPath, "dataflow-models")) } - classDirs, err := approximation.ResolveAll(absPaths, newModelBuilder(analyzerJarPath)) + classDirs, err := models.ResolveAll(absPaths, newModelBuilder(analyzerJarPath)) if err != nil { out.Fatalf("Failed to apply dataflow models: %s", err) } diff --git a/cli/cmd/compile_approximations.go b/cli/cmd/compile_models.go similarity index 92% rename from cli/cmd/compile_approximations.go rename to cli/cmd/compile_models.go index 923c65cb98..f2d6629eb3 100644 --- a/cli/cmd/compile_approximations.go +++ b/cli/cmd/compile_models.go @@ -5,9 +5,9 @@ import ( "os" "path/filepath" - "github.com/seqra/opentaint/internal/approximation" "github.com/seqra/opentaint/internal/autobuilder" "github.com/seqra/opentaint/internal/globals" + "github.com/seqra/opentaint/internal/models" "github.com/seqra/opentaint/internal/output" ) @@ -18,7 +18,7 @@ type modelBuilder struct { apiJar []byte } -func newModelBuilder(analyzerJarPath string) approximation.Builder { +func newModelBuilder(analyzerJarPath string) models.Builder { return &modelBuilder{analyzerJarPath: analyzerJarPath} } @@ -44,13 +44,13 @@ func (b *modelBuilder) Fingerprint() (string, error) { // The builder reads the jar from the analyzer only once. func (b *modelBuilder) Prepare(projectDir string) error { if b.apiJar == nil { - apiJar, err := approximation.ExtractApiJar(b.analyzerJarPath) + apiJar, err := models.ExtractApiJar(b.analyzerJarPath) if err != nil { return err } b.apiJar = apiJar } - return approximation.WriteApiJar(projectDir, b.apiJar) + return models.WriteApiJar(projectDir, b.apiJar) } func (b *modelBuilder) Compile(projectDir, projectModelDir string) error { diff --git a/cli/cmd/compile_approximations_cmd.go b/cli/cmd/compile_models_cmd.go similarity index 85% rename from cli/cmd/compile_approximations_cmd.go rename to cli/cmd/compile_models_cmd.go index 5a2ebe77fa..fa2a4a613f 100644 --- a/cli/cmd/compile_approximations_cmd.go +++ b/cli/cmd/compile_models_cmd.go @@ -3,7 +3,7 @@ package cmd import ( "fmt" - "github.com/seqra/opentaint/internal/approximation" + "github.com/seqra/opentaint/internal/models" "github.com/seqra/opentaint/internal/utils/log" "github.com/spf13/cobra" ) @@ -26,7 +26,7 @@ needed to compile ahead of time or to see compilation errors on their own.`, Run: func(cmd *cobra.Command, args []string) { projectPath := log.AbsPathOrExit(args[0], "model-project") - if !approximation.IsProject(projectPath) { + if !models.IsProject(projectPath) { out.Fatalf("%s is not a model project: it has no build.gradle.kts declaring the "+ "dependencies its models are written against.\nCreate one with 'opentaint model init %s "+ "--dependency '", projectPath, args[0]) @@ -34,7 +34,7 @@ needed to compile ahead of time or to see compilation errors on their own.`, outputPath := compileModelsOutput if outputPath == "" { - outputPath = approximation.BuildDir(projectPath) + outputPath = models.BuildDir(projectPath) } else { outputPath = log.AbsPathOrExit(outputPath, "output") } @@ -44,13 +44,13 @@ needed to compile ahead of time or to see compilation errors on their own.`, out.Fatalf("Failed to resolve analyzer: %s", err) } - if err := approximation.Build(projectPath, outputPath, newModelBuilder(analyzerJarPath)); err != nil { + if err := models.Build(projectPath, outputPath, newModelBuilder(analyzerJarPath)); err != nil { out.Fatalf("Model compilation failed: %s", err) } out.Section("Model Compile Summary"). Field("Model project", projectPath). - Field("Compiled models written to", approximation.ClassesDir(outputPath)). + Field("Compiled models written to", models.ClassesDir(outputPath)). Render() suggest("To apply these models in a scan run", fmt.Sprintf("opentaint scan --dataflow-models %s", args[0])) diff --git a/cli/cmd/approximation.go b/cli/cmd/model.go similarity index 93% rename from cli/cmd/approximation.go rename to cli/cmd/model.go index ca289348db..371b4ed129 100644 --- a/cli/cmd/approximation.go +++ b/cli/cmd/model.go @@ -3,7 +3,7 @@ package cmd import ( "fmt" - "github.com/seqra/opentaint/internal/approximation" + "github.com/seqra/opentaint/internal/models" "github.com/seqra/opentaint/internal/utils/log" "github.com/spf13/cobra" ) @@ -46,13 +46,13 @@ nothing about the project under analysis, decide what the models compile against // Init replaces the build file but keeps the model sources. // Warn the user because this removes dependency pins that they added manually. - existing := approximation.IsProject(projectPath) + existing := models.IsProject(projectPath) if existing { out.Warnf("Replacing the build file of the existing model project at %s; "+ "pass every dependency the models need, not only the new ones", projectPath) } - if err := approximation.Scaffold(projectPath, initModelDeps, analyzerJarPath); err != nil { + if err := models.Scaffold(projectPath, initModelDeps, analyzerJarPath); err != nil { out.Fatalf("Failed to create model project: %s", err) } diff --git a/cli/cmd/test_init.go b/cli/cmd/test_init.go index c7aab49d91..b22181be31 100644 --- a/cli/cmd/test_init.go +++ b/cli/cmd/test_init.go @@ -4,7 +4,7 @@ import ( "fmt" "path/filepath" - "github.com/seqra/opentaint/internal/testapprox" + "github.com/seqra/opentaint/internal/testmodel" "github.com/seqra/opentaint/internal/testproject" "github.com/seqra/opentaint/internal/testrule" "github.com/spf13/cobra" @@ -63,7 +63,7 @@ var testModelInitCmd = &cobra.Command{ 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 + - model-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 Positive and negative samples are specified via rule-test.yaml. @@ -74,10 +74,10 @@ The model under test is supplied separately at test time with Use --dependency to add compile-only Maven dependencies for the samples.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - if err := testproject.Bootstrap(args[0], "approximation-test-project", initModelProjectDeps); err != nil { + if err := testproject.Bootstrap(args[0], "model-test-project", initModelProjectDeps); err != nil { out.Fatalf("Failed to bootstrap test project: %s", err) } - if err := testapprox.Scaffold(args[0]); err != nil { + if err := testmodel.Scaffold(args[0]); err != nil { out.Fatalf("Failed to scaffold model test project: %s", err) } fmt.Printf("Model test project initialized at %s\n", args[0]) diff --git a/cli/cmd/test_approximation_run.go b/cli/cmd/test_model_run.go similarity index 85% rename from cli/cmd/test_approximation_run.go rename to cli/cmd/test_model_run.go index 0e81b29f7f..9fd68c7504 100644 --- a/cli/cmd/test_approximation_run.go +++ b/cli/cmd/test_model_run.go @@ -4,7 +4,7 @@ import ( "os" "time" - "github.com/seqra/opentaint/internal/testapprox" + "github.com/seqra/opentaint/internal/testmodel" "github.com/spf13/cobra" ) @@ -21,16 +21,16 @@ var testModelRunCmd = &cobra.Command{ Long: `Run the samples specified in rule-test.yaml with the supplied dataflow models applied. A built-in source-to-sink harness rule is applied automatically; positive samples reference the -approximation-rule.yaml rule with id "approximation-rule". +model-rule.yaml rule with id "model-rule". ` + testExitCodesHelp("All model tests passed"), Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - ruleDir, err := os.MkdirTemp("", "opentaint-approx-rule-*") + ruleDir, err := os.MkdirTemp("", "opentaint-model-rule-*") if err != nil { out.Fatalf("Failed to create temp dir for harness rule: %s", err) } - if _, err := testapprox.WriteFixedRule(ruleDir); err != nil { + if _, err := testmodel.WriteFixedRule(ruleDir); err != nil { out.Fatalf("Failed to materialize harness rule: %s", err) } diff --git a/cli/internal/approximation/build.go b/cli/internal/models/build.go similarity index 93% rename from cli/internal/approximation/build.go rename to cli/internal/models/build.go index 20f1a89d97..b661072f35 100644 --- a/cli/internal/approximation/build.go +++ b/cli/internal/models/build.go @@ -1,4 +1,4 @@ -package approximation +package models import ( "errors" @@ -144,9 +144,9 @@ func build(projectDir, outputDir string, inputs stamp, builder Builder) error { } sweepStaging(outputDir) - buildTempDir, err := os.MkdirTemp("", "opentaint-approximation-build-*") + buildTempDir, err := os.MkdirTemp("", "opentaint-model-build-*") if err != nil { - return fmt.Errorf("failed to create temp directory for approximation build: %w", err) + return fmt.Errorf("failed to create temp directory for model build: %w", err) } defer func() { _ = os.RemoveAll(buildTempDir) }() @@ -164,7 +164,7 @@ func build(projectDir, outputDir string, inputs stamp, builder Builder) error { return fmt.Errorf("failed to clear %s: %w", staging, err) } if err := os.MkdirAll(ClassesDir(staging), 0o755); err != nil { - return fmt.Errorf("failed to create approximation build output %s: %w", staging, err) + return fmt.Errorf("failed to create model build output %s: %w", staging, err) } dependencies, err := collectClasses(projectModelDir, ClassesDir(staging)) @@ -172,7 +172,7 @@ func build(projectDir, outputDir string, inputs stamp, builder Builder) error { return err } if !containsClassFiles(ClassesDir(staging)) { - return fmt.Errorf("approximation project %s compiled to no classes; it has no models under src/main/java", projectDir) + return fmt.Errorf("model project %s compiled to no classes; it has no models under src/main/java", projectDir) } if err := writeDescriptor(staging, descriptor{ @@ -216,7 +216,7 @@ func checkOutputDir(outputDir string) error { } if !empty { return fmt.Errorf( - "output directory %s already exists and is not an approximation build output; "+ + "output directory %s already exists and is not a model build output; "+ "remove it or choose another --output path", outputDir) } return nil @@ -270,7 +270,7 @@ func isEmptyDir(dir string) (bool, error) { func collectClasses(projectModelDir, destDir string) ([]string, error) { config, err := project.LoadConfig(projectModelDir) if err != nil { - return nil, fmt.Errorf("approximation build produced no readable project model: %w", err) + return nil, fmt.Errorf("model build produced no readable project model: %w", err) } for _, module := range config.AllModules() { @@ -310,7 +310,7 @@ func copyTree(srcDir, destDir string) error { } if _, err := os.Stat(target); err == nil { return fmt.Errorf( - "two approximation modules produce %s; an approximation class maps to exactly one target class", + "two model modules produce %s; a model class maps to exactly one target class", filepath.ToSlash(rel), ) } diff --git a/cli/internal/approximation/descriptor.go b/cli/internal/models/descriptor.go similarity index 92% rename from cli/internal/approximation/descriptor.go rename to cli/internal/models/descriptor.go index 1fd66b4046..a578f3bd74 100644 --- a/cli/internal/approximation/descriptor.go +++ b/cli/internal/models/descriptor.go @@ -1,4 +1,4 @@ -package approximation +package models import ( "fmt" @@ -34,7 +34,7 @@ func writeDescriptor(outputDir string, d descriptor) error { } body, err := yaml.Marshal(d) if err != nil { - return fmt.Errorf("failed to render the approximation descriptor: %w", err) + return fmt.Errorf("failed to render the model descriptor: %w", err) } path := filepath.Join(outputDir, descriptorName) if err := os.WriteFile(path, append([]byte(descriptorHeader), body...), 0o644); err != nil { diff --git a/cli/internal/approximation/approximation_test.go b/cli/internal/models/models_test.go similarity index 99% rename from cli/internal/approximation/approximation_test.go rename to cli/internal/models/models_test.go index a8122791b9..2bf513b759 100644 --- a/cli/internal/approximation/approximation_test.go +++ b/cli/internal/models/models_test.go @@ -1,4 +1,4 @@ -package approximation +package models import ( "os" @@ -486,7 +486,7 @@ func TestBuildRefusesToOverwriteUnrelatedDirectory(t *testing.T) { if err == nil { t.Fatal("expected a build into an unrelated directory to be refused") } - if !strings.Contains(err.Error(), "not an approximation build output") { + if !strings.Contains(err.Error(), "not a model build output") { t.Errorf("unexpected error: %v", err) } if content, readErr := os.ReadFile(precious); readErr != nil || string(content) != "important user data" { @@ -542,7 +542,7 @@ func TestBuildRejectsCollidingModelClasses(t *testing.T) { err := Build(project, BuildDir(project), builder) if err == nil { - t.Fatal("expected colliding approximation classes to fail the build") + t.Fatal("expected colliding model classes to fail the build") } if !strings.Contains(err.Error(), "exactly one target class") { t.Errorf("unexpected error: %v", err) diff --git a/cli/internal/approximation/project.go b/cli/internal/models/project.go similarity index 98% rename from cli/internal/approximation/project.go rename to cli/internal/models/project.go index eb8a721d94..ed257c2f71 100644 --- a/cli/internal/approximation/project.go +++ b/cli/internal/models/project.go @@ -1,8 +1,8 @@ -// Package approximation manages Gradle projects for dataflow models. +// Package models manages Gradle projects for dataflow models. // // Each project specifies the library versions that its models use. // The scanned project does not supply these compile dependencies. -package approximation +package models import ( "bytes" diff --git a/cli/internal/approximation/resolve.go b/cli/internal/models/resolve.go similarity index 87% rename from cli/internal/approximation/resolve.go rename to cli/internal/models/resolve.go index 168ac2eba2..c3b670c87a 100644 --- a/cli/internal/approximation/resolve.go +++ b/cli/internal/models/resolve.go @@ -1,4 +1,4 @@ -package approximation +package models import ( "fmt" @@ -59,7 +59,7 @@ func checkClasspathAgrees(classDirs []string) error { return fmt.Errorf( "%s is offered with different content by %s and by %s; which one an analysis "+ "reads is not defined.\nDrop one of the copies, or pass only one of these "+ - "approximation paths", + "model paths", class, previous, dir) } } @@ -77,10 +77,10 @@ func checkClasspathAgrees(classDirs []string) error { func Resolve(path string, builder Builder) ([]string, error) { info, err := os.Stat(path) if err != nil { - return nil, fmt.Errorf("approximation path does not exist: %w", err) + return nil, fmt.Errorf("model path does not exist: %w", err) } if !info.IsDir() { - return nil, fmt.Errorf("approximation path %s is not a directory", path) + return nil, fmt.Errorf("model path %s is not a directory", path) } classDirs, err := resolveDir(path, builder) @@ -118,7 +118,7 @@ func resolveDir(dir string, builder Builder) ([]string, error) { // order the filesystem happens to hand them back. entries, err := os.ReadDir(dir) if err != nil { - return nil, fmt.Errorf("failed to read approximation directory %s: %w", dir, err) + return nil, fmt.Errorf("failed to read model directory %s: %w", dir, err) } // Descending past classes this directory holds itself would leave them unused. @@ -141,12 +141,12 @@ func resolveDir(dir string, builder Builder) ([]string, error) { } // ambiguousDirError reports a directory that is neither a class directory nor a -// directory of approximation paths. +// directory of model paths. func ambiguousDirError(dir, classFile string) error { return fmt.Errorf( - "%s holds compiled classes of its own (e.g. %s) and an approximation project or build "+ + "%s holds compiled classes of its own (e.g. %s) and a model project or build "+ "output below it, so it is neither a class directory nor a directory of them.\n"+ - "Move the classes into a directory of their own, or pass each approximation path to "+ + "Move the classes into a directory of their own, or pass each model path to "+ "--dataflow-models separately", dir, classFile) } @@ -155,12 +155,12 @@ func ambiguousDirError(dir, classFile string) error { func emptyPathError(path string) error { if source, found := firstJavaSource(path); found { return fmt.Errorf( - "%s holds approximation sources (e.g. %s) but no approximation project: nothing declares "+ + "%s holds model sources (e.g. %s) but no model project: nothing declares "+ "the dependencies to compile the models against.\n"+ "Create a project per model directory with 'opentaint model init "+ "--dependency ' and move its sources under /src/main/java", path, source, ) } - return fmt.Errorf("%s contains no approximation project, build output or compiled classes", path) + return fmt.Errorf("%s contains no model project, build output or compiled classes", path) } diff --git a/cli/internal/approximation/scaffold.go b/cli/internal/models/scaffold.go similarity index 90% rename from cli/internal/approximation/scaffold.go rename to cli/internal/models/scaffold.go index d3d9b1e09d..578b7893fe 100644 --- a/cli/internal/approximation/scaffold.go +++ b/cli/internal/models/scaffold.go @@ -1,4 +1,4 @@ -package approximation +package models import ( "archive/zip" @@ -11,7 +11,7 @@ import ( "github.com/seqra/opentaint/internal/testproject" ) -const gradleProjectName = "approximation-models" +const gradleProjectName = "dependency-models" // Scaffold creates a Gradle project for dataflow models. func Scaffold(projectDir string, dependencies []string, analyzerJarPath string) error { @@ -30,7 +30,7 @@ func Scaffold(projectDir string, dependencies []string, analyzerJarPath string) return os.MkdirAll(filepath.Join(projectDir, "src", "main", "java"), 0o755) } -// ExtractApiJar reads the approximation API jar from the analyzer jar. +// ExtractApiJar reads the model API jar from the analyzer jar. func ExtractApiJar(analyzerJarPath string) ([]byte, error) { reader, err := zip.OpenReader(analyzerJarPath) if err != nil { @@ -56,7 +56,7 @@ func readZipEntry(file *zip.File) ([]byte, error) { return io.ReadAll(content) } -// WriteApiJar writes the approximation API jar to a model project. +// WriteApiJar writes the model API jar to a model project. // It does not write the file when the content is current. func WriteApiJar(projectDir string, apiJar []byte) error { path := apiJarPath(projectDir) diff --git a/cli/internal/approximation/stamp.go b/cli/internal/models/stamp.go similarity index 95% rename from cli/internal/approximation/stamp.go rename to cli/internal/models/stamp.go index 98cd6323b2..58df0cd362 100644 --- a/cli/internal/approximation/stamp.go +++ b/cli/internal/models/stamp.go @@ -1,4 +1,4 @@ -package approximation +package models import ( "crypto/sha256" @@ -43,7 +43,7 @@ func computeSourceDigest(projectDir string) (string, error) { return nil }) if err != nil { - return "", fmt.Errorf("failed to read approximation project %s: %w", projectDir, err) + return "", fmt.Errorf("failed to read model project %s: %w", projectDir, err) } sort.Strings(paths) diff --git a/cli/internal/testapprox/example/approximation-rule.yaml b/cli/internal/testmodel/example/model-rule.yaml similarity index 72% rename from cli/internal/testapprox/example/approximation-rule.yaml rename to cli/internal/testmodel/example/model-rule.yaml index 0f0ce705ae..1905d51ae7 100644 --- a/cli/internal/testapprox/example/approximation-rule.yaml +++ b/cli/internal/testmodel/example/model-rule.yaml @@ -1,9 +1,9 @@ rules: - - id: approximation-rule + - id: model-rule severity: ERROR - message: Tainted value from Taint.source() reached Taint.sink() through an approximated method + message: Tainted value from Taint.source() reached Taint.sink() through a modeled method metadata: - short-description: Approximation test source-to-sink flow + short-description: Model test source-to-sink flow languages: - java mode: taint diff --git a/cli/internal/testapprox/example/src/main/java/test/Taint.java b/cli/internal/testmodel/example/src/main/java/test/Taint.java similarity index 100% rename from cli/internal/testapprox/example/src/main/java/test/Taint.java rename to cli/internal/testmodel/example/src/main/java/test/Taint.java diff --git a/cli/internal/testapprox/testapprox.go b/cli/internal/testmodel/testmodel.go similarity index 77% rename from cli/internal/testapprox/testapprox.go rename to cli/internal/testmodel/testmodel.go index 3a686c11c2..3a3b525232 100644 --- a/cli/internal/testapprox/testapprox.go +++ b/cli/internal/testmodel/testmodel.go @@ -1,4 +1,4 @@ -package testapprox +package testmodel import ( _ "embed" @@ -9,9 +9,9 @@ import ( "github.com/seqra/opentaint/internal/utils" ) -const fixedRuleFileName = "approximation-rule.yaml" +const fixedRuleFileName = "model-rule.yaml" -//go:embed example/approximation-rule.yaml +//go:embed example/model-rule.yaml var fixedRule []byte //go:embed example/src/main/java/test/Taint.java @@ -20,7 +20,7 @@ var taintJava []byte func WriteFixedRule(dir string) (string, error) { path := filepath.Join(dir, fixedRuleFileName) if err := os.WriteFile(path, fixedRule, 0o644); err != nil { - return "", fmt.Errorf("write fixed approximation rule: %w", err) + return "", fmt.Errorf("write fixed model rule: %w", err) } return path, nil } From 4cd2aa9ae457febc1cb07a1e312672d4edf8ed2c Mon Sep 17 00:00:00 2001 From: Gr-i-niy Date: Mon, 2 Nov 2026 12:12:44 +0000 Subject: [PATCH 3/4] feat(cli): accept --passthrough-models in test model run A model test can now apply pass-through models next to the dataflow models under test, as test rule run already does. --- cli/cmd/deprecated_test.go | 2 +- cli/cmd/test.go | 3 ++- cli/cmd/test_model_run.go | 26 ++++++++++++++------------ cli/cmd/test_rule_run.go | 3 +-- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cli/cmd/deprecated_test.go b/cli/cmd/deprecated_test.go index 89dd128eaa..959fe6e1b7 100644 --- a/cli/cmd/deprecated_test.go +++ b/cli/cmd/deprecated_test.go @@ -70,7 +70,7 @@ func TestDeprecatedFlagAliasesFeedTheNewFlag(t *testing.T) { } func TestModelFlagsOnAnalyzerCommands(t *testing.T) { - for _, command := range []*cobra.Command{scanCmd, testRuleRunCmd} { + for _, command := range []*cobra.Command{scanCmd, testRuleRunCmd, testModelRunCmd} { for _, name := range []string{"dataflow-models", "dataflow-approximations", "passthrough-models", "passthrough-approximations"} { if command.Flags().Lookup(name) == nil { t.Errorf("%s has no --%s", command.CommandPath(), name) diff --git a/cli/cmd/test.go b/cli/cmd/test.go index eaca915602..f6080b78a4 100644 --- a/cli/cmd/test.go +++ b/cli/cmd/test.go @@ -47,9 +47,10 @@ func testExitCodesHelp(passedLine string) string { 255 Project configuration error` } -func addTestRunFlags(cmd *cobra.Command, outputDir *string, timeout *time.Duration, maxMemory *string, dataflow *[]string) { +func addTestRunFlags(cmd *cobra.Command, outputDir *string, timeout *time.Duration, maxMemory *string, dataflow, passthrough *[]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)") addDataflowModelsFlag(cmd, dataflow) + addPassthroughModelsFlag(cmd, passthrough) } diff --git a/cli/cmd/test_model_run.go b/cli/cmd/test_model_run.go index 9fd68c7504..88eabec863 100644 --- a/cli/cmd/test_model_run.go +++ b/cli/cmd/test_model_run.go @@ -9,10 +9,11 @@ import ( ) var ( - testModelOutputDir string - testModelTimeout time.Duration - testModelMaxMemory string - testModelDataflow []string + testModelOutputDir string + testModelTimeout time.Duration + testModelMaxMemory string + testModelDataflow []string + testModelPassthrough []string ) var testModelRunCmd = &cobra.Command{ @@ -35,19 +36,20 @@ model-rule.yaml rule with id "model-rule". } runTestProject(args[0], testProjectOptions{ - label: "Model tests", - tempDir: "opentaint-test-models-*", - rulesets: []string{ruleDir}, - outputDir: testModelOutputDir, - timeout: testModelTimeout, - maxMemory: testModelMaxMemory, - dataflowModels: testModelDataflow, + label: "Model tests", + tempDir: "opentaint-test-models-*", + rulesets: []string{ruleDir}, + outputDir: testModelOutputDir, + timeout: testModelTimeout, + maxMemory: testModelMaxMemory, + dataflowModels: testModelDataflow, + passthroughModels: testModelPassthrough, }) }, } func init() { testModelCmd.AddCommand(testModelRunCmd) - addTestRunFlags(testModelRunCmd, &testModelOutputDir, &testModelTimeout, &testModelMaxMemory, &testModelDataflow) + addTestRunFlags(testModelRunCmd, &testModelOutputDir, &testModelTimeout, &testModelMaxMemory, &testModelDataflow, &testModelPassthrough) testApproximationCmd.AddCommand(deprecatedCommand(testModelRunCmd, "run", "opentaint test model run")) } diff --git a/cli/cmd/test_rule_run.go b/cli/cmd/test_rule_run.go index cb7575d571..4e24c732ad 100644 --- a/cli/cmd/test_rule_run.go +++ b/cli/cmd/test_rule_run.go @@ -171,7 +171,6 @@ func init() { testRuleCmd.AddCommand(testRuleRunCmd) testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleset, "ruleset", nil, "Ruleset file or directory to test (repeatable)") - addTestRunFlags(testRuleRunCmd, &testRulesOutputDir, &testRulesTimeout, &testRulesMaxMemory, &testRulesDataflow) + addTestRunFlags(testRuleRunCmd, &testRulesOutputDir, &testRulesTimeout, &testRulesMaxMemory, &testRulesDataflow, &testRulesPassthrough) testRuleRunCmd.Flags().StringArrayVar(&testRulesRuleID, "rule-id", nil, "Run only rules with this ID (repeatable)") - addPassthroughModelsFlag(testRuleRunCmd, &testRulesPassthrough) } From 5bfff95d94d0b5abab09fb209bd96600e0f63f6c Mon Sep 17 00:00:00 2001 From: Gr-i-niy Date: Fri, 30 Oct 2026 10:07:02 +0000 Subject: [PATCH 4/4] feat(cli): print the analyzer log path on an analyzer exception An unhandled analyzer exception (exit code 252) is only described in the analyzer log, which lives in a temporary directory. test rule run and test model run now print its path. scan already points at its own log file, which holds the analyzer output, so it prints the analyzer log only when it has no log file. --- cli/cmd/command_builder.go | 8 ++++++++ cli/cmd/command_builder_test.go | 13 +++++++++++++ cli/cmd/scan.go | 3 +++ cli/cmd/suggest.go | 14 ++++++++++++++ cli/cmd/suggest_test.go | 25 +++++++++++++++++++++++++ cli/cmd/test.go | 2 +- cli/cmd/test_rule_run.go | 3 +++ 7 files changed, 67 insertions(+), 1 deletion(-) diff --git a/cli/cmd/command_builder.go b/cli/cmd/command_builder.go index 263c45e58c..428454c7b6 100644 --- a/cli/cmd/command_builder.go +++ b/cli/cmd/command_builder.go @@ -69,6 +69,13 @@ type AnalyzerBuilder struct { debugFactReachabilitySarif bool runRuleTests bool debugRunAnalysisOnSelectedEntryPoints string + // logsFile is the analyzer log of the last command built. + logsFile string +} + +// LogsFile returns the path of the analyzer log that the last command from BuildNativeCommand writes. +func (a *AnalyzerBuilder) LogsFile() string { + return a.logsFile } func (a *AnalyzerBuilder) SetProject(projectPath string) *AnalyzerBuilder { @@ -193,6 +200,7 @@ func (a *AnalyzerBuilder) BuildNativeCommand() []string { return []string{} } tempLogsFile := filepath.Join(tempLogsDir, "analyzer.log") + a.logsFile = tempLogsFile command := []string{ a.maxMemory, diff --git a/cli/cmd/command_builder_test.go b/cli/cmd/command_builder_test.go index 903585a70f..58777087a8 100644 --- a/cli/cmd/command_builder_test.go +++ b/cli/cmd/command_builder_test.go @@ -1,6 +1,8 @@ package cmd import ( + "os" + "path/filepath" "reflect" "testing" ) @@ -63,6 +65,17 @@ func TestAppendVerbosityFlagPreservesExistingFlags(t *testing.T) { } } +// The analyzer log path is reported after an analyzer exception, so it must be the one the command writes. +func TestAnalyzerBuilderReportsItsLogsFile(t *testing.T) { + b := NewAnalyzerBuilder().SetProject("/project/project.yaml").SetOutputDir("/output") + cmd := b.BuildNativeCommand() + t.Cleanup(func() { _ = os.RemoveAll(filepath.Dir(b.LogsFile())) }) + + if filepath.Base(b.LogsFile()) != "analyzer.log" || !containsFlagPair(cmd, "--logs-file", b.LogsFile()) { + t.Fatalf("LogsFile() = %q, command was %v", b.LogsFile(), cmd) + } +} + // containsFlagPair reports whether args contains the adjacent pair `flag value`. func containsFlagPair(args []string, flag, value string) bool { for i := 0; i+1 < len(args); i++ { diff --git a/cli/cmd/scan.go b/cli/cmd/scan.go index 33c94898ab..6df354bae9 100644 --- a/cli/cmd/scan.go +++ b/cli/cmd/scan.go @@ -467,6 +467,9 @@ func runScan(cmd *cobra.Command, cfg ScanConfig) { var suggestions []output.Suggestion if analyzerFail != nil { suggestions = appendLogSuggestion(suggestions) + if analyzerLog, ok := analyzerLogSuggestion(analyzerFail, nativeBuilder.LogsFile()); ok { + suggestions = append(suggestions, analyzerLog) + } } if report != nil { // Scan does not expose summary's filter/group flags, so pass zero values: diff --git a/cli/cmd/suggest.go b/cli/cmd/suggest.go index 9508a86cb0..bd64b5a54d 100644 --- a/cli/cmd/suggest.go +++ b/cli/cmd/suggest.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/seqra/opentaint/internal/analyzer" "github.com/seqra/opentaint/internal/globals" "github.com/seqra/opentaint/internal/output" ) @@ -30,6 +31,19 @@ func logSuggestion() (output.Suggestion, bool) { }, true } +// analyzerLogSuggestion points at the analyzer log after an unhandled analyzer exception +// (exit code 252), whose details are only in that log. The second result is false for other +// outcomes, and when the run has its own log file, which already holds the analyzer output. +func analyzerLogSuggestion(fail *analyzer.Error, logsFile string) (output.Suggestion, bool) { + if fail == nil || fail.ExitCode != analyzer.ExitException || logsFile == "" || globals.LogPath != "" { + return output.Suggestion{}, false + } + return output.Suggestion{ + Description: "For the analyzer exception, check the analyzer log:", + Command: logsFile, + }, 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. diff --git a/cli/cmd/suggest_test.go b/cli/cmd/suggest_test.go index 570eb7690b..60b232d26a 100644 --- a/cli/cmd/suggest_test.go +++ b/cli/cmd/suggest_test.go @@ -3,6 +3,7 @@ package cmd import ( "testing" + "github.com/seqra/opentaint/internal/analyzer" "github.com/seqra/opentaint/internal/globals" "github.com/seqra/opentaint/internal/output" ) @@ -60,3 +61,27 @@ func TestBuildFailSuggestions(t *testing.T) { t.Errorf("expected no suggestions, got %+v", got) } } + +func TestAnalyzerLogSuggestion(t *testing.T) { + orig := globals.LogPath + t.Cleanup(func() { globals.LogPath = orig }) + globals.LogPath = "" + + for code, want := range map[int]bool{analyzer.ExitException: true, analyzer.ExitOOM: false, analyzer.ExitTimeout: false, 1: false} { + sug, ok := analyzerLogSuggestion(&analyzer.Error{ExitCode: code}, "/tmp/opentaint-1/analyzer.log") + if ok != want { + t.Errorf("exit %d: suggestion shown = %v, want %v", code, ok, want) + } + if ok && sug.Command != "/tmp/opentaint-1/analyzer.log" { + t.Errorf("exit %d: command = %q, want the analyzer log", code, sug.Command) + } + } + if _, ok := analyzerLogSuggestion(nil, "/tmp/analyzer.log"); ok { + t.Error("a successful run points at the analyzer log") + } + + globals.LogPath = "/tmp/run.log" + if _, ok := analyzerLogSuggestion(&analyzer.Error{ExitCode: analyzer.ExitException}, "/tmp/analyzer.log"); ok { + t.Error("a run with its own log file also points at the analyzer log") + } +} diff --git a/cli/cmd/test.go b/cli/cmd/test.go index f6080b78a4..2142a7c757 100644 --- a/cli/cmd/test.go +++ b/cli/cmd/test.go @@ -41,7 +41,7 @@ func testExitCodesHelp(passedLine string) string { 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 + 252 Unhandled analyzer exception (the analyzer log path is printed) 253 Out of memory (try increasing --max-memory) 254 Analysis timed out (try increasing --timeout) 255 Project configuration error` diff --git a/cli/cmd/test_rule_run.go b/cli/cmd/test_rule_run.go index 4e24c732ad..3f78deb108 100644 --- a/cli/cmd/test_rule_run.go +++ b/cli/cmd/test_rule_run.go @@ -150,6 +150,9 @@ func runTestProject(projectModelArg string, opts testProjectOptions) { fmt.Printf("Test results: %s\n", resultPath) if analyzerFail != nil { + if analyzerLog, ok := analyzerLogSuggestion(analyzerFail, builder.LogsFile()); ok { + out.Suggestions(analyzerLog) + } os.Exit(analyzerFail.ExitCode) }