github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/tui/model/state_initialize.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package model
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"deps.dev/util/resolve"
    23  	"github.com/charmbracelet/bubbles/spinner"
    24  	tea "github.com/charmbracelet/bubbletea"
    25  	"github.com/google/osv-scalibr/guidedremediation/internal/lockfile"
    26  	"github.com/google/osv-scalibr/guidedremediation/internal/manifest"
    27  	"github.com/google/osv-scalibr/guidedremediation/internal/parser"
    28  	"github.com/google/osv-scalibr/guidedremediation/internal/remediation"
    29  	"github.com/google/osv-scalibr/guidedremediation/internal/strategy/inplace"
    30  	"github.com/google/osv-scalibr/guidedremediation/internal/tui/components"
    31  	"github.com/google/osv-scalibr/guidedremediation/options"
    32  	"github.com/google/osv-scalibr/guidedremediation/result"
    33  )
    34  
    35  type stateInitialize struct {
    36  	spinner spinner.Model
    37  }
    38  
    39  func newStateInitialize() stateInitialize {
    40  	return stateInitialize{
    41  		spinner: components.NewSpinner(),
    42  	}
    43  }
    44  
    45  func (s stateInitialize) Init(m Model) tea.Cmd {
    46  	cmds := []tea.Cmd{s.spinner.Tick}
    47  	if m.options.Lockfile != "" {
    48  		// if we have a lockfile, start calculating the in-place updates
    49  		cmds = append(cmds, doInPlaceResolutionCmd(m.options, m.lockfileRW))
    50  	} else {
    51  		// if we don't have a lockfile, start calculating the relock result
    52  		cmds = append(cmds, doInitialRelockCmd(m.options, m.manifestRW))
    53  	}
    54  
    55  	return tea.Batch(cmds...)
    56  }
    57  
    58  func (s stateInitialize) Update(m Model, msg tea.Msg) (tea.Model, tea.Cmd) {
    59  	var c tea.Cmd
    60  	s.spinner, c = s.spinner.Update(msg)
    61  	m.st = s
    62  	cmds := []tea.Cmd{c}
    63  	switch msg := msg.(type) {
    64  	// in-place resolution finished
    65  	case inPlaceResolutionMsg:
    66  		if msg.err != nil {
    67  			return errorAndExit(m, msg.err)
    68  		}
    69  		// set the result and start the relock computation
    70  		m.lockfileGraph = msg.resolvedGraph
    71  		m.lockfilePatches = msg.allPatches
    72  		if m.options.Manifest != "" {
    73  			cmds = append(cmds, doInitialRelockCmd(m.options, m.manifestRW))
    74  		} else {
    75  			m.st = newStateChooseStrategy(m)
    76  			cmds = append(cmds, m.st.Init(m))
    77  		}
    78  
    79  	// relocking finished
    80  	case doRelockMsg:
    81  		if msg.err != nil {
    82  			return errorAndExit(m, msg.err)
    83  		}
    84  		// set the result and go to next state
    85  		m.relockBaseManifest = msg.resolvedManifest
    86  		m.relockBaseErrors = computeResolveErrors(msg.resolvedManifest.Graph)
    87  		if m.options.Lockfile == "" {
    88  			m.st = stateRelockResult{}
    89  		} else {
    90  			m.st = newStateChooseStrategy(m)
    91  		}
    92  		cmds = append(cmds, m.st.Init(m))
    93  	}
    94  
    95  	return m, tea.Batch(cmds...)
    96  }
    97  
    98  func (s stateInitialize) View(m Model) string {
    99  	sb := strings.Builder{}
   100  	if m.options.Lockfile == "" {
   101  		sb.WriteString("No lockfile provided. Assuming re-lock.\n")
   102  	} else {
   103  		sb.WriteString(fmt.Sprintf("Scanning %s ", components.SelectedTextStyle.Render(m.options.Lockfile)))
   104  		if m.lockfileGraph.Graph == nil {
   105  			sb.WriteString(s.spinner.View())
   106  			sb.WriteString("\n")
   107  
   108  			return sb.String()
   109  		}
   110  		sb.WriteString("✓\n")
   111  	}
   112  
   113  	sb.WriteString(fmt.Sprintf("Resolving %s ", components.SelectedTextStyle.Render(m.options.Manifest)))
   114  	if m.relockBaseManifest == nil {
   115  		sb.WriteString(s.spinner.View())
   116  		sb.WriteString("\n")
   117  	} else {
   118  		sb.WriteString("✓\n")
   119  	}
   120  
   121  	return sb.String()
   122  }
   123  
   124  func (s stateInitialize) InfoView() string               { return "" }
   125  func (s stateInitialize) Resize(_, _ int) modelState     { return s }
   126  func (s stateInitialize) ResizeInfo(_, _ int) modelState { return s }
   127  func (s stateInitialize) IsInfoFocused() bool            { return false }
   128  
   129  type inPlaceResolutionMsg struct {
   130  	resolvedGraph remediation.ResolvedGraph
   131  	allPatches    []result.Patch
   132  	err           error
   133  }
   134  
   135  func doInPlaceResolutionCmd(opts options.FixVulnsOptions, rw lockfile.ReadWriter) tea.Cmd {
   136  	return func() tea.Msg {
   137  		g, err := parser.ParseLockfile(opts.Lockfile, rw)
   138  		if err != nil {
   139  			return inPlaceResolutionMsg{err: err}
   140  		}
   141  
   142  		resolved, err := remediation.ResolveGraphVulns(context.Background(), opts.ResolveClient, opts.VulnEnricher, g, nil, &opts.RemediationOptions)
   143  		if err != nil {
   144  			return inPlaceResolutionMsg{err: fmt.Errorf("failed resolving lockfile vulnerabilities: %w", err)}
   145  		}
   146  		allPatches, err := inplace.ComputePatches(context.Background(), opts.ResolveClient, resolved, &opts.RemediationOptions)
   147  		if err != nil {
   148  			return inPlaceResolutionMsg{err: fmt.Errorf("failed computing patches: %w", err)}
   149  		}
   150  		return inPlaceResolutionMsg{resolvedGraph: resolved, allPatches: allPatches}
   151  	}
   152  }
   153  
   154  type doRelockMsg struct {
   155  	resolvedManifest *remediation.ResolvedManifest
   156  	err              error
   157  }
   158  
   159  func doInitialRelockCmd(opts options.FixVulnsOptions, rw manifest.ReadWriter) tea.Cmd {
   160  	return func() tea.Msg {
   161  		m, err := parser.ParseManifest(opts.Manifest, rw)
   162  		if err != nil {
   163  			return doRelockMsg{err: err}
   164  		}
   165  		if opts.DepCachePopulator != nil {
   166  			opts.DepCachePopulator.PopulateCache(context.Background(), opts.ResolveClient, m.Requirements(), opts.Manifest)
   167  		}
   168  		resolved, err := remediation.ResolveManifest(context.Background(), opts.ResolveClient, opts.VulnEnricher, m, &opts.RemediationOptions)
   169  		if err != nil {
   170  			return doRelockMsg{err: fmt.Errorf("failed resolving manifest vulnerabilities: %w", err)}
   171  		}
   172  		return doRelockMsg{resolvedManifest: resolved}
   173  	}
   174  }
   175  
   176  func computeResolveErrors(g *resolve.Graph) []result.ResolveError {
   177  	var errs []result.ResolveError
   178  	for _, n := range g.Nodes {
   179  		for _, e := range n.Errors {
   180  			errs = append(errs, result.ResolveError{
   181  				Package: result.Package{
   182  					Name:    n.Version.Name,
   183  					Version: n.Version.Version,
   184  				},
   185  				Requirement: result.Package{
   186  					Name:    e.Req.Name,
   187  					Version: e.Req.Version,
   188  				},
   189  				Error: e.Error,
   190  			})
   191  		}
   192  	}
   193  
   194  	return errs
   195  }