Skip to content

Commit

Permalink
wip: scan state aggregator
Browse files Browse the repository at this point in the history
  • Loading branch information
ShawkyZ committed Jan 20, 2025
1 parent 1396db2 commit c090af6
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 6 deletions.
8 changes: 3 additions & 5 deletions domain/ide/workspace/folder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand Down
179 changes: 179 additions & 0 deletions domain/snyk/aggregator/scan_state_aggregator.go
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 39 additions & 0 deletions domain/snyk/aggregator/summary_emitter.go
Original file line number Diff line number Diff line change
@@ -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

Check failure on line 29 in domain/snyk/aggregator/summary_emitter.go

View workflow job for this annotation

GitHub Actions / lint

field `notifier` is unused (unused)
scanStateAggregator *ScanStateAggregator

Check failure on line 30 in domain/snyk/aggregator/summary_emitter.go

View workflow job for this annotation

GitHub Actions / lint

field `scanStateAggregator` is unused (unused)
c *config.Config

Check failure on line 31 in domain/snyk/aggregator/summary_emitter.go

View workflow job for this annotation

GitHub Actions / lint

field `c` is unused (unused)
}

func (s *SummaryEmitter) Emit() {
// Generate HTML
// Send notification to LS
//generatedHtml := "foobar"
//s.notifier.Send()
}
26 changes: 26 additions & 0 deletions domain/snyk/delta/delta_provider.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion domain/snyk/scan_result_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions domain/snyk/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down

0 comments on commit c090af6

Please sign in to comment.