Skip to content
Merged
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
10 changes: 5 additions & 5 deletions .github/workflows/ci-cli.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
- name: Run opentaint scan in native environment when user can build project but has no java 17+
working-directory: cli
run: |
./opentaint scan ${{ steps.github-token.outputs.arg }} --output report.sarif ../project-root-java-11 --verbosity debug
./opentaint scan --debug ${{ steps.github-token.outputs.arg }} --output report.sarif ../project-root-java-11

run-on-petclinic:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -140,8 +140,8 @@ jobs:
- name: Run opentaint separate compile and scan
working-directory: cli
run: |
./opentaint compile --quiet ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root --verbosity debug
./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project --verbosity debug
./opentaint compile --quiet --debug ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root
./opentaint scan --quiet --debug ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project

- name: Run opentaint scan with explicit path and output
working-directory: cli
Expand Down Expand Up @@ -265,8 +265,8 @@ jobs:
- name: Run opentaint separate compile and scan
working-directory: cli
run: |
./opentaint compile --quiet ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root --verbosity debug
./opentaint scan --quiet ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project --verbosity debug
./opentaint compile --quiet --debug ${{ steps.github-token.outputs.arg }} --output portable-project ../project-root
./opentaint scan --quiet --debug ${{ steps.github-token.outputs.arg }} --output report.sarif --project-model portable-project

- name: Run opentaint scan
working-directory: cli
Expand Down
2 changes: 1 addition & 1 deletion cli/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ COPY --from=builder /app/opentaint /usr/local/lib/opentaint/opentaint
RUN ln -sf /usr/local/lib/opentaint/opentaint /usr/local/bin/opentaint

RUN --mount=type=secret,id=github_token \
opentaint pull --github-token=$(cat /run/secrets/github_token) --verbosity debug
opentaint pull --github-token=$(cat /run/secrets/github_token) --debug

CMD ["opentaint", "--help"]
17 changes: 8 additions & 9 deletions cli/cmd/command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,23 @@ import (
)

type BaseCommandBuilder struct {
verbosity string
debug bool
}

// appendVerbosityFlag passes the JAR's own --verbosity flag based on opentaint's
// debug bool. The Java tool's CLI surface is independent of opentaint's; we just
// translate. info ↔ false, debug ↔ true.
func (b *BaseCommandBuilder) appendVerbosityFlag(flags []string) []string {
switch b.verbosity {
case "info":
return append(flags, "--verbosity=info")
case "debug":
if b.debug {
return append(flags, "--verbosity=debug")
default:
return flags
}
return append(flags, "--verbosity=info")
}

func NewAnalyzerBuilder() *AnalyzerBuilder {
return &AnalyzerBuilder{
BaseCommandBuilder: &BaseCommandBuilder{
verbosity: globals.Config.Log.Verbosity,
debug: globals.Config.Output.Debug,
},
maxMemory: "-Xmx8G",
}
Expand All @@ -35,7 +34,7 @@ func NewAnalyzerBuilder() *AnalyzerBuilder {
func NewAutobuilderBuilder() *AutobuilderBuilder {
return &AutobuilderBuilder{
BaseCommandBuilder: &BaseCommandBuilder{
verbosity: globals.Config.Log.Verbosity,
debug: globals.Config.Output.Debug,
},
maxMemory: "-Xmx1G",
}
Expand Down
64 changes: 64 additions & 0 deletions cli/cmd/command_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cmd

import (
"reflect"
"testing"
)

func TestHasNestedKey(t *testing.T) {
settings := map[string]any{
"log": map[string]any{
"verbosity": "debug",
"color": "auto",
},
"quiet": true,
}
tests := []struct {
name string
path []string
want bool
}{
{name: "top-level key present", path: []string{"quiet"}, want: true},
{name: "nested key present", path: []string{"log", "verbosity"}, want: true},
{name: "nested key missing", path: []string{"log", "missing"}, want: false},
{name: "parent exists but not a map", path: []string{"quiet", "sub"}, want: false},
{name: "empty path", path: []string{}, want: false},
{name: "missing top-level", path: []string{"other"}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hasNestedKey(settings, tt.path); got != tt.want {
t.Fatalf("hasNestedKey(%v) = %t, want %t", tt.path, got, tt.want)
}
})
}
}

func TestAppendVerbosityFlag(t *testing.T) {
tests := []struct {
name string
debug bool
want []string
}{
{name: "debug off emits info", debug: false, want: []string{"--verbosity=info"}},
{name: "debug on emits debug", debug: true, want: []string{"--verbosity=debug"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &BaseCommandBuilder{debug: tt.debug}
got := b.appendVerbosityFlag(nil)
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("appendVerbosityFlag(debug=%t) = %v, want %v", tt.debug, got, tt.want)
}
})
}
}

func TestAppendVerbosityFlagPreservesExistingFlags(t *testing.T) {
b := &BaseCommandBuilder{debug: true}
got := b.appendVerbosityFlag([]string{"--project", "foo.yaml"})
want := []string{"--project", "foo.yaml", "--verbosity=debug"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("appendVerbosityFlag did not preserve prior flags: got %v, want %v", got, want)
}
}
3 changes: 1 addition & 2 deletions cli/cmd/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Arguments:

sb := out.Section("OpenTaint Compile")
addConfigFields(cmd, sb)
if globals.Config.Log.Verbosity == "debug" {
if globals.Config.Output.Debug {
sb.Line()
}
sb.Field("Project", absProjectRoot).
Expand All @@ -88,7 +88,6 @@ Arguments:

compileJavaRunner := java.NewJavaRunner().
WithSkipVerify(globals.Config.SkipVerify).
WithStreamOutput(globals.Config.Quiet).
WithDebugOutput(out.DebugStream("Autobuilder")).
TrySystem().
TrySpecificVersion(globals.Config.Java.Version)
Expand Down
1 change: 0 additions & 1 deletion cli/cmd/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ func (c *JavaAutobuilderConfig) runAutobuilder() error {

javaRunner := java.NewJavaRunner().
WithSkipVerify(globals.Config.SkipVerify).
WithStreamOutput(globals.Config.Quiet).
WithDebugOutput(out.DebugStream("Autobuilder")).
WithImageType(java.AdoptiumImageJRE).
TrySpecificVersion(globals.DefaultJavaVersion)
Expand Down
58 changes: 30 additions & 28 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,19 @@ var rootCmd = &cobra.Command{
SilenceUsage: true,

PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
verbosity, err := normalizeVerbosity(globals.Config.Log.Verbosity)
if err != nil {
return err
}
globals.Config.Log.Verbosity = verbosity

if err := log.SetUpLogs(globals.Config.Log.Verbosity); err != nil {
if err := log.SetUpLogs(); err != nil {
return fmt.Errorf("failed to set up logging: %w", err)
}

// Configure the output printer (color mode, quiet mode)
out.Configure(globals.Config.Log.Color, globals.Config.Quiet)
out.SetVerbosity(globals.Config.Log.Verbosity)
// Configure the output printer.
out.Configure(globals.Config.Output.Color, globals.Config.Output.Quiet)
out.SetDebug(globals.Config.Output.Debug)

// Reconcile install-tier version marker if needed (lightweight: a few Stat calls).
utils.ReconcileInstallMarker()

// Start async update check (non-blocking, at most once per day)
if !globals.Config.Quiet {
if !globals.Config.Output.Quiet {
go checkForUpdateAsync()
}

Expand Down Expand Up @@ -98,14 +92,14 @@ func init() {

rootCmd.Flags().BoolVarP(&toolVersion, "version", "v", false, "Print the version information")

rootCmd.PersistentFlags().StringVar(&globals.Config.Log.Verbosity, "verbosity", "info", "Verbosity level (info, debug)")
_ = viper.BindPFlag("log.verbosity", rootCmd.PersistentFlags().Lookup("verbosity"))
rootCmd.PersistentFlags().BoolVarP(&globals.Config.Output.Debug, "debug", "d", false, "Enable debug output (stream JAR subprocess output, show debug-only fields)")
_ = viper.BindPFlag("output.debug", rootCmd.PersistentFlags().Lookup("debug"))

rootCmd.PersistentFlags().StringVar(&globals.Config.Log.Color, "color", "auto", "Color mode (auto, always, never)")
_ = viper.BindPFlag("log.color", rootCmd.PersistentFlags().Lookup("color"))
rootCmd.PersistentFlags().StringVar(&globals.Config.Output.Color, "color", "auto", "Color mode (auto, always, never)")
_ = viper.BindPFlag("output.color", rootCmd.PersistentFlags().Lookup("color"))

rootCmd.PersistentFlags().BoolVarP(&globals.Config.Quiet, "quiet", "q", false, "Suppress interactive UI elements (spinners, progress bars)")
_ = viper.BindPFlag("quiet", rootCmd.PersistentFlags().Lookup("quiet"))
rootCmd.PersistentFlags().BoolVarP(&globals.Config.Output.Quiet, "quiet", "q", false, "Suppress interactive UI elements (spinners, progress bars) and JAR streaming")
_ = viper.BindPFlag("output.quiet", rootCmd.PersistentFlags().Lookup("quiet"))

rootCmd.PersistentFlags().StringVar(&globals.Config.Analyzer.Version, "analyzer-version", globals.AnalyzerBindVersion, "Version of opentaint analyzer")
_ = rootCmd.PersistentFlags().MarkHidden("analyzer-version")
Expand Down Expand Up @@ -153,23 +147,31 @@ func initConfig() {
_ = viper.Unmarshal(&globals.Config)
}

func normalizeVerbosity(level string) (string, error) {
value := strings.ToLower(strings.TrimSpace(level))
switch value {
case "", "info":
return "info", nil
case "debug":
return "debug", nil
default:
return "info", nil
// hasNestedKey reports whether a dotted key path is present in a viper settings map.
// Each path segment must resolve to a non-nil value; intermediate segments must be maps.
func hasNestedKey(m map[string]any, parts []string) bool {
if len(parts) == 0 {
return false
}
v, ok := m[parts[0]]
if !ok {
return false
}
if len(parts) == 1 {
return true
}
nested, ok := v.(map[string]any)
if !ok {
return false
}
return hasNestedKey(nested, parts[1:])
}

// addConfigFields appends config fields to a SectionBuilder if PrintConfig annotation is set.
func addConfigFields(cmd *cobra.Command, sb *output.SectionBuilder) {
if cmd.Annotations != nil && cmd.Annotations["PrintConfig"] == "true" {
if globals.Config.Log.Verbosity == "debug" {
sb.Field("Log level", globals.Config.Log.Verbosity)
if globals.Config.Output.Debug {
sb.Field("Log level", "debug")
if viper.ConfigFileUsed() != "" {
sb.Field("Config file", viper.ConfigFileUsed())
}
Expand Down
4 changes: 1 addition & 3 deletions cli/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,6 @@ func scan(cmd *cobra.Command) {

compileJavaRunner := java.NewJavaRunner().
WithSkipVerify(globals.Config.SkipVerify).
WithStreamOutput(globals.Config.Quiet).
WithDebugOutput(out.DebugStream("Autobuilder")).
TrySystem().
TrySpecificVersion(globals.Config.Java.Version)
Expand Down Expand Up @@ -344,7 +343,6 @@ func scan(cmd *cobra.Command) {

analyzerJavaRunner := java.NewJavaRunner().
WithSkipVerify(globals.Config.SkipVerify).
WithStreamOutput(globals.Config.Quiet).
WithDebugOutput(out.DebugStream("Analyzer")).
WithImageType(java.AdoptiumImageJRE).
TrySpecificVersion(globals.DefaultJavaVersion)
Expand Down Expand Up @@ -467,7 +465,7 @@ func resolveScanConfig(absUserProjectRoot string) scanConfig {
func printScanInfo(cmd *cobra.Command, cfg scanConfig, absSemgrepRuleLoadTracePath string, absUserProjectRoot string, absRuleSetPaths []RulesetType) {
sb := out.Section(cfg.mode.String())
addConfigFields(cmd, sb)
if globals.Config.Log.Verbosity == "debug" {
if globals.Config.Output.Debug {
sb.Field("Rule load trace", absSemgrepRuleLoadTracePath)
sb.Line()
}
Expand Down
12 changes: 6 additions & 6 deletions cli/internal/globals/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ type Scan struct {
CodeFlowLimit int64 `mapstructure:"code_flow_limit"`
}

type Log struct {
Verbosity string `mapstructure:"verbosity"`
Color string `mapstructure:"color"`
type Output struct {
Debug bool `mapstructure:"debug"`
Color string `mapstructure:"color"`
Quiet bool `mapstructure:"quiet"`
}

type Github struct {
Expand All @@ -72,8 +73,8 @@ type Java struct {
}

type ConfigType struct {
Scan Scan `mapstructure:"scan"`
Log Log `mapstructure:"log"`
Scan Scan `mapstructure:"scan"`
Output Output `mapstructure:"output"`

Github Github `mapstructure:"github"`
Analyzer Analyzer `mapstructure:"analyzer"`
Expand All @@ -82,7 +83,6 @@ type ConfigType struct {
Java Java `mapstructure:"java"`
Owner string `mapstructure:"owner"`
Repo string `mapstructure:"repo"`
Quiet bool `mapstructure:"quiet"`
SkipVerify bool `mapstructure:"skip-verify"`
}

Expand Down
2 changes: 1 addition & 1 deletion cli/internal/load_trace/rule_statistics_tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func buildRuleParsingIssuesNode(out *output.Printer, result *RuleLoadErrorsResul
}

s := result.Summary
isDebug := globals.Config.Log.Verbosity == "debug"
isDebug := globals.Config.Output.Debug

var children []any

Expand Down
35 changes: 18 additions & 17 deletions cli/internal/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ import (
// through a Printer instance. It holds the active theme, the output writer,
// and knows whether it's writing to a TTY.
type Printer struct {
baseW io.Writer
w io.Writer
debugW io.Writer
logW io.Writer
verbosity string
theme *Theme
isTTY bool
quiet bool
profile colorprofile.Profile
baseW io.Writer
w io.Writer
debugW io.Writer
logW io.Writer
debug bool
theme *Theme
isTTY bool
quiet bool
profile colorprofile.Profile
}

var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`)
Expand Down Expand Up @@ -119,21 +119,22 @@ func (p *Printer) SetLogWriter(w io.Writer) {
p.logW = w
}

// SetVerbosity stores the configured log verbosity to control
// interactive UI elements like spinners and progress bars.
func (p *Printer) SetVerbosity(level string) {
p.verbosity = strings.ToLower(strings.TrimSpace(level))
// SetDebug enables debug-mode console output: JAR streaming, debug-only
// fields in info sections, and suppression of spinners (which would conflict
// with streamed JAR output).
func (p *Printer) SetDebug(debug bool) {
p.debug = debug
}

// IsDebugVerbosity returns true for debug-like verbosity modes.
func (p *Printer) IsDebugVerbosity() bool {
return p.verbosity == "debug"
// IsDebug returns true when debug-mode console output is enabled.
func (p *Printer) IsDebug() bool {
return p.debug
}

// IsInteractiveUI returns true when interactive UI components
// (spinners/progress bars) should be rendered.
func (p *Printer) IsInteractiveUI() bool {
return p.IsInteractive() && !p.IsDebugVerbosity() && !p.quiet
return p.IsInteractive() && !p.IsDebug() && !p.quiet
}

func (p *Printer) writeMirroredLine(line string) {
Expand Down
Loading
Loading