Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/cmd/analyzer_inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import (

func addDataflowModelsFlag(cmd *cobra.Command, paths *[]string) {
cmd.Flags().StringArrayVar(paths, "dataflow-models", nil,
"Dataflow model project, build output, or class directory (repeatable)")
"Dataflow models: a 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)")
"Pass-through models: a YAML file or a directory of them (repeatable)")
addDeprecatedFlagAlias(cmd.Flags(), "passthrough-models", "passthrough-approximations")
}

Expand Down
42 changes: 29 additions & 13 deletions cli/cmd/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/spf13/cobra"

"github.com/seqra/opentaint/internal/globals"
"github.com/seqra/opentaint/internal/testmodel"
"github.com/seqra/opentaint/internal/testproject"
"github.com/seqra/opentaint/internal/utils"
"github.com/seqra/opentaint/internal/utils/java"
"github.com/seqra/opentaint/internal/utils/log"
Expand All @@ -32,21 +34,22 @@ 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 Java or Kotlin project into a 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: `Detect the build system of a Java or Kotlin project, compile the project, and write a project model that "opentaint scan --project-model" analyzes without a new build

Arguments:
project - Path to a project to compile (required)
`,
<project> Root directory of the project`,
Example: ` # Compile a project into ./app-model
opentaint compile ./app -o ./app-model`,
Annotations: map[string]string{"PrintConfig": "true"},
Run: func(cmd *cobra.Command, args []string) {
ProjectPath = args[0]
Expand All @@ -72,33 +75,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(compileNextStep(absProjectRoot, projectRoot, absOutputProjectModelPath))
} else {
out.InteractiveBlank()
failWith(1, fmt.Sprintf("Native compile has failed: %s", err), dockerCompileSuggestion())
Expand All @@ -109,10 +112,10 @@ 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 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)")
compileCmd.Flags().StringVar(&CompileLogFile, "log-file", "", logFileUsage)
}

func compile(absProjectRoot, absOutputProjectModelPath, autobuilderJarPath string, javaRunner java.JavaRunner) error {
Expand Down Expand Up @@ -190,3 +193,16 @@ func compileProjectWithLogs(absOutputProjectModelPath, absProjectRoot, autobuild

return nil
}

// compileNextStep returns the hint printed after a successful compile. A compiled test project is
// tested, not scanned.
func compileNextStep(absProjectRoot, projectRoot, absOutputProjectModelPath string) (string, string) {
if !utils.PathExists(filepath.Join(absProjectRoot, testproject.RuleTestFileName)) {
return "To scan the compiled project model, run:", utils.BuildScanCommandFromCompile(projectRoot, absOutputProjectModelPath)
}
projectModel := shellQuote(absOutputProjectModelPath)
if testmodel.IsTestProject(absProjectRoot) {
return "To run the model tests, run:", "opentaint test model run " + projectModel + " --dataflow-models <model>"
}
return "To run the rule tests, run:", "opentaint test rule run " + projectModel
}
15 changes: 5 additions & 10 deletions cli/cmd/compile_models_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,10 @@ var compileModelsOutput string
var compileModelsCmd = &cobra.Command{
Use: "models <model-project>",
Short: "Compile a dataflow model project",
Long: `Compile the dataflow models in <model-project> against the
dependencies that project pins in its own build file.
Long: `Compile the dataflow models of a model project against the library versions that its build.gradle.kts pins

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 a model project on demand, so running this is only
needed to compile ahead of time or to see compilation errors on their own.`,
Arguments:
<model-project> Model project created with "opentaint model init"`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
projectPath := log.AbsPathOrExit(args[0], "model-project")
Expand Down Expand Up @@ -52,8 +47,8 @@ needed to compile ahead of time or to see compilation errors on their own.`,
Field("Model project", projectPath).
Field("Compiled models written to", models.ClassesDir(outputPath)).
Render()
suggest("To apply these models in a scan run",
fmt.Sprintf("opentaint scan <project-model> --dataflow-models %s", args[0]))
suggest("To scan with these models, run:",
fmt.Sprintf("opentaint scan <source-path> --dataflow-models %s", shellQuote(args[0])))
},
}

Expand Down
48 changes: 48 additions & 0 deletions cli/cmd/compile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cmd

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/seqra/opentaint/internal/testmodel"
"github.com/seqra/opentaint/internal/testproject"
"github.com/seqra/opentaint/internal/testrule"
)

func TestCompileNextStep(t *testing.T) {
plain := t.TempDir()
if err := os.WriteFile(filepath.Join(plain, "pom.xml"), nil, 0o644); err != nil {
t.Fatal(err)
}
ruleTest := t.TempDir()
if err := testrule.Scaffold(ruleTest); err != nil {
t.Fatal(err)
}
modelTest := t.TempDir()
if err := testmodel.Scaffold(modelTest); err != nil {
t.Fatal(err)
}

for name, tc := range map[string]struct{ dir, want string }{
"project": {plain, "opentaint scan "},
"rule tests": {ruleTest, "opentaint test rule run /out"},
"model tests": {modelTest, "opentaint test model run /out --dataflow-models <model>"},
"rule-test.yaml only": {writeRuleTest(t), "opentaint test rule run /out"},
} {
t.Run(name, func(t *testing.T) {
if _, command := compileNextStep(tc.dir, tc.dir, "/out"); !strings.HasPrefix(command, tc.want) {
t.Errorf("command = %q, want the prefix %q", command, tc.want)
}
})
}
}

func writeRuleTest(t *testing.T) string {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, testproject.RuleTestFileName), []byte("tests: []\n"), 0o644); err != nil {
t.Fatal(err)
}
return dir
}
20 changes: 20 additions & 0 deletions cli/cmd/deprecated.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ func deprecatedCommand(cmd *cobra.Command, oldName, newPath string) *cobra.Comma
return alias
}

// deprecatedGroup returns a hidden command group under the old name of group. Its subcommands are
// deprecated too, so cobra lists none of them. The help of the old group is therefore the help of
// group, after the deprecation notice.
func deprecatedGroup(group *cobra.Command, oldName, newPath string) *cobra.Command {
alias := &cobra.Command{
Use: oldName,
Short: group.Short,
Deprecated: "use '" + newPath + "' instead",
}
alias.SetHelpFunc(func(c *cobra.Command, args []string) {
if c != alias {
// The subcommands inherit this function. They keep the help of the root.
c.Root().HelpFunc()(c, args)
return
}
_ = group.Help()
})
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) {
Expand Down
30 changes: 30 additions & 0 deletions cli/cmd/deprecated_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"bytes"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -78,3 +79,32 @@ func TestModelFlagsOnAnalyzerCommands(t *testing.T) {
}
}
}

func TestDeprecatedGroupHelpIsTheHelpOfTheNewGroup(t *testing.T) {
var buf bytes.Buffer
rootCmd.SetOut(&buf)
t.Cleanup(func() { rootCmd.SetOut(nil) })

for _, tc := range []struct {
old []string
want string
}{
{[]string{"approximation"}, "opentaint model [command]"},
{[]string{"test", "approximation"}, "opentaint test model [command]"},
// A subcommand of a deprecated group keeps its own help.
{[]string{"approximation", "init"}, "opentaint approximation init <output-dir>"},
{[]string{"test", "approximation", "run"}, "opentaint test approximation run <project-model>"},
} {
t.Run(strings.Join(tc.old, " "), func(t *testing.T) {
command, _, err := rootCmd.Find(tc.old)
if err != nil {
t.Fatal(err)
}
buf.Reset()
command.HelpFunc()(command, nil)
if !strings.Contains(buf.String(), tc.want) {
t.Errorf("help does not contain %q:\n%s", tc.want, buf.String())
}
})
}
}
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
}
42 changes: 42 additions & 0 deletions cli/cmd/exit_codes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cmd

import (
"fmt"
"strings"

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

// analyzerExitCodeRows lists the analyzer exit codes. exceptionNote, when set, follows the
// unhandled exception row.
func analyzerExitCodeRows(exceptionNote string) string {
rows := ""
for _, code := range []int{analyzer.ExitException, analyzer.ExitOOM, analyzer.ExitTimeout, analyzer.ExitConfigError} {
rows += fmt.Sprintf("\n %-3d %s", code, capitalize(analyzer.ExitMessage(code)))
if code == analyzer.ExitException && exceptionNote != "" {
rows += " " + exceptionNote
}
}
return rows
}

// capitalize upper-cases the first letter, so the analyzer rows match the rows above them.
func capitalize(s string) string {
if s == "" {
return s
}
return strings.ToUpper(s[:1]) + s[1:]
}

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("(the analyzer log path is printed)")
}
16 changes: 7 additions & 9 deletions cli/cmd/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,12 @@ 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 the paths of the analyzer, autobuilder, built-in rules, and Java runtime",
Long: `Show the versions and paths of the analyzer, autobuilder, built-in rules, and Java runtime. The command exits with a nonzero code when a component is missing

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.

The exit code is non-zero when any selected component is missing.`,
With one component flag, the command prints only the path of that component`,
Example: ` # Store the path of the built-in rules for a script
rules_dir=$(opentaint health --rules)`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runHealth()
Expand All @@ -46,7 +43,7 @@ func init() {
rootCmd.AddCommand(healthCmd)
healthCmd.Flags().BoolVar(&healthAutobuilder, "autobuilder", false, "Print only the autobuilder JAR path")
healthCmd.Flags().BoolVar(&healthAnalyzer, "analyzer", false, "Print only the analyzer JAR path")
healthCmd.Flags().BoolVar(&healthRules, "rules", false, "Print only the built-in rules path, downloading rules if needed")
healthCmd.Flags().BoolVar(&healthRules, "rules", false, "Print only the built-in rules path (downloads the rules when missing)")
healthCmd.Flags().BoolVar(&healthRuntime, "runtime", false, "Print only the Java runtime path")
}

Expand Down Expand Up @@ -105,6 +102,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