diff --git a/domain/ide/workspace/folder.go b/domain/ide/workspace/folder.go index 404f39f0c..e0c8e604d 100644 --- a/domain/ide/workspace/folder.go +++ b/domain/ide/workspace/folder.go @@ -472,9 +472,7 @@ func appendTestResults(sic snyk.SeverityIssueCounts, results []json_schemas.Test } func (f *Folder) FilterAndPublishDiagnostics(p product.Product) { - issueByProduct := f.IssuesByProduct() - - productIssuesByFile, err := f.getDelta(issueByProduct, p) + productIssuesByFile, err := f.GetDelta(p) if err != nil { // Error can only be returned from delta analysis. Other non delta scans are skipped with no errors. err = fmt.Errorf("couldn't determine the difference between current and base branch for %s scan. %w", p.ToProductNamesString(), err) @@ -495,9 +493,9 @@ func (f *Folder) FilterAndPublishDiagnostics(p product.Product) { f.publishDiagnostics(p, filteredIssuesToSend, err) } -// Error can only be returned from delta analysis. Other non delta scans are skipped with no errors. -func (f *Folder) getDelta(productIssueByFile snyk.ProductIssuesByFile, p product.Product) (snyk.ProductIssuesByFile, error) { +func (f *Folder) GetDelta(p product.Product) (snyk.ProductIssuesByFile, error) { logger := f.c.Logger().With().Str("method", "getDelta").Logger() + productIssueByFile := f.IssuesByProduct() if !f.c.IsDeltaFindingsEnabled() { return productIssueByFile, nil } diff --git a/domain/snyk/aggregator/scan_state_aggregator.go b/domain/snyk/aggregator/scan_state_aggregator.go new file mode 100644 index 000000000..eeff0335a --- /dev/null +++ b/domain/snyk/aggregator/scan_state_aggregator.go @@ -0,0 +1,179 @@ +/* + * © 2025 Snyk Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aggregator + +import ( + "sync" + + "github.com/rs/zerolog" + + "github.com/snyk/snyk-ls/domain/ide/workspace" + "github.com/snyk/snyk-ls/internal/product" +) + +// FolderProductKey identifies a unique (FolderPath, ProductName) pair. +type FolderProductKey struct { + FolderPath string + Product product.Product +} + +type ScanStatus string + +const ( + NotStarted ScanStatus = "NOT_STARTED" + InProgress ScanStatus = "IN_PROGRESS" + Done ScanStatus = "DONE" + Error ScanStatus = "ERROR" +) + +// ScanState describes the state for one scan (per folder+product). +type ScanState struct { + Status ScanStatus + Err error +} + +type ScanStateMap map[FolderProductKey]*ScanState + +// ScanStateAggregator stores and manages the scan states for working directory and reference scans. +type ScanStateAggregator struct { + mu sync.RWMutex + referenceScanStates ScanStateMap + workingDirectoryScanStates ScanStateMap + scanStateChangeEmitter ScanStateChangeEmitter + logger *zerolog.Logger +} + +// NewScanStateAggregator constructs a new aggregator. +func NewScanStateAggregator(ssce ScanStateChangeEmitter, ws *workspace.Workspace) *ScanStateAggregator { + res := &ScanStateAggregator{ + referenceScanStates: make(ScanStateMap), + workingDirectoryScanStates: make(ScanStateMap), + scanStateChangeEmitter: ssce, + } + for _, f := range ws.Folders() { + res.referenceScanStates[FolderProductKey{Product: product.ProductOpenSource, FolderPath: f.Path()}] = &ScanState{Status: NotStarted} + res.referenceScanStates[FolderProductKey{Product: product.ProductCode, FolderPath: f.Path()}] = &ScanState{Status: NotStarted} + res.referenceScanStates[FolderProductKey{Product: product.ProductInfrastructureAsCode, FolderPath: f.Path()}] = &ScanState{Status: NotStarted} + + res.workingDirectoryScanStates[FolderProductKey{Product: product.ProductOpenSource, FolderPath: f.Path()}] = &ScanState{Status: NotStarted} + res.workingDirectoryScanStates[FolderProductKey{Product: product.ProductCode, FolderPath: f.Path()}] = &ScanState{Status: NotStarted} + res.workingDirectoryScanStates[FolderProductKey{Product: product.ProductInfrastructureAsCode, FolderPath: f.Path()}] = &ScanState{Status: NotStarted} + } + + return res +} + +// SetScanState changes the Status field of the existing state (or creates it if it doesn't exist). +func (agg *ScanStateAggregator) SetScanState(folderPath string, p product.Product, isReferenceScan bool, newState ScanState) { + agg.mu.Lock() + defer agg.mu.Unlock() + + key := FolderProductKey{FolderPath: folderPath, Product: p} + var st *ScanState + var exists bool + if isReferenceScan { + st, exists = agg.referenceScanStates[key] + } else { + st, exists = agg.workingDirectoryScanStates[key] + } + + if !exists { + agg.logger.Warn().Msgf("Scan State for folder path%s and product %s doesn't exist in state aggregator", folderPath, p.ToProductNamesString()) + return + } + + st.Status = newState.Status + st.Err = newState.Err + + agg.scanStateChangeEmitter.Emit() +} + +func (agg *ScanStateAggregator) AreAllScansNotStarted(isReference bool) bool { + agg.mu.RLock() + defer agg.mu.RUnlock() + + var stateMap ScanStateMap + if isReference { + stateMap = agg.referenceScanStates + } else { + stateMap = agg.workingDirectoryScanStates + } + + for _, st := range stateMap { + if st.Status != NotStarted { + return false + } + } + return true +} + +func (agg *ScanStateAggregator) HasAnyScanInProgress(isReference bool) bool { + agg.mu.RLock() + defer agg.mu.RUnlock() + + var stateMap ScanStateMap + if isReference { + stateMap = agg.referenceScanStates + } else { + stateMap = agg.workingDirectoryScanStates + } + + for _, st := range stateMap { + if st.Status == InProgress { + return true + } + } + return false +} + +func (agg *ScanStateAggregator) HaveAllScansSucceeded(isReference bool) bool { + agg.mu.RLock() + defer agg.mu.RUnlock() + + var stateMap ScanStateMap + if isReference { + stateMap = agg.referenceScanStates + } else { + stateMap = agg.workingDirectoryScanStates + } + + for _, st := range stateMap { + if st.Status != Done || st.Err != nil { + return false + } + } + return true +} + +func (agg *ScanStateAggregator) HasAnyScanError(isReference bool) bool { + agg.mu.RLock() + defer agg.mu.RUnlock() + + var stateMap ScanStateMap + if isReference { + stateMap = agg.referenceScanStates + } else { + stateMap = agg.workingDirectoryScanStates + } + + for _, st := range stateMap { + if st.Status == Error { + return true + } + } + return false +} diff --git a/domain/snyk/aggregator/summary_emitter.go b/domain/snyk/aggregator/summary_emitter.go new file mode 100644 index 000000000..6c59eadfe --- /dev/null +++ b/domain/snyk/aggregator/summary_emitter.go @@ -0,0 +1,39 @@ +/* + * © 2025 Snyk Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aggregator + +import ( + "github.com/snyk/snyk-ls/application/config" + "github.com/snyk/snyk-ls/internal/notification" +) + +type ScanStateChangeEmitter interface { + Emit() +} + +type SummaryEmitter struct { + notifier notification.Notifier + scanStateAggregator *ScanStateAggregator + c *config.Config +} + +func (s *SummaryEmitter) Emit() { + // Generate HTML + // Send notification to LS + //generatedHtml := "foobar" + //s.notifier.Send() +} diff --git a/domain/snyk/delta/delta_provider.go b/domain/snyk/delta/delta_provider.go new file mode 100644 index 000000000..269d21d7d --- /dev/null +++ b/domain/snyk/delta/delta_provider.go @@ -0,0 +1,26 @@ +/* + * © 2025 Snyk Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package delta + +import ( + "github.com/snyk/snyk-ls/domain/snyk" + "github.com/snyk/snyk-ls/internal/product" +) + +type Provider interface { + GetDelta(p product.Product) (snyk.ProductIssuesByFile, error) +} diff --git a/domain/snyk/scan_result_processor.go b/domain/snyk/scan_result_processor.go index 1c91ca7c9..8a7a966c6 100644 --- a/domain/snyk/scan_result_processor.go +++ b/domain/snyk/scan_result_processor.go @@ -41,7 +41,7 @@ type IssueCount struct { Ignored int } -func NoopResultProcessor(_ ScanData) {} +func NoopResultProcessor(_ ScanData, _ bool, _ bool) {} func (s ScanData) GetSeverityIssueCounts() SeverityIssueCounts { sic := make(SeverityIssueCounts) diff --git a/domain/snyk/scanner/scanner.go b/domain/snyk/scanner/scanner.go index 4cb19ea26..1eb66cf0a 100644 --- a/domain/snyk/scanner/scanner.go +++ b/domain/snyk/scanner/scanner.go @@ -300,6 +300,7 @@ func (sc *DelegatingConcurrentScanner) Scan( processResults(data, true, true) go func() { defer referenceBranchScanWaitGroup.Done() + // TODO: implement proper context handling err := sc.scanBaseBranch(context.Background(), s, folderPath, gitCheckoutHandler) // TODO: is this a good idea? data = snyk.ScanData{