Skip to content
4 changes: 2 additions & 2 deletions cli/cmd/analyzer_inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"))
}
}
38 changes: 25 additions & 13 deletions cli/cmd/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,33 @@ 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 <project>",
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: `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.

Arguments:
project - Path to a project to compile (required)
`,
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.

Later scans can use the model without a new build. This makes repeated scans fast.

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

# 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]
Expand All @@ -72,33 +84,33 @@ 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 {
return compile(absProjectRoot, absOutputProjectModelPath, autobuilderJarPath, compileJavaRunner)
}); err == nil {
out.Blank()
printCompileSummary(absOutputProjectModelPath)
suggest("To scan project run", utils.BuildScanCommandFromCompile(projectRoot, absOutputProjectModelPath))
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())
Expand All @@ -109,7 +121,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: <cache-dir>/logs/<timestamp>.log)")
Expand Down
2 changes: 1 addition & 1 deletion cli/cmd/compile_approximations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 34 additions & 2 deletions cli/cmd/dry_run.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cmd

import "fmt"
import (
"os"
"strings"
)

func failOnInvalidInputs(validate func() error) {
if err := validate(); err != nil {
Expand All @@ -9,5 +12,34 @@ func failOnInvalidInputs(validate func() error) {
}

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())
}

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, " ")
}

func shellQuote(arg string) string {
if arg != "" && !strings.ContainsFunc(arg, shellUnsafe) {
return arg
}
return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'"
}

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
}
28 changes: 28 additions & 0 deletions cli/cmd/exit_codes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package cmd

import (
"fmt"

"github.com/seqra/opentaint/internal/analyzer"
)

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
}

func scanExitCodesHelp(completedLine string) string {
return `Exit codes:
0 ` + completedLine + `
1 General failure (configuration or infrastructure error)` + analyzerExitCodeRows()
}

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()
}
35 changes: 35 additions & 0 deletions cli/cmd/flag_alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package cmd

import (
"strings"

"github.com/spf13/pflag"
)

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"
}

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)
}
}
56 changes: 56 additions & 0 deletions cli/cmd/flag_alias_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
26 changes: 19 additions & 7 deletions cli/cmd/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,26 @@ 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 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.

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.
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 exit code is non-zero when any selected component is missing.`,
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 a script
opentaint health --analyzer

# 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()
Expand Down Expand Up @@ -105,6 +116,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
Expand Down
Loading
Loading