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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.12.0] 2026-03-10
### Added
- Restore action: Undo a previous decision (include, dismiss, replace) on a completed result, returning it to the pending state
- Keyboard shortcut `U` to restore a file decision
- Add support for `file_snippet` settings in `scanoss.json`, including ranking, snippet hit/line thresholds, file extension handling, and header skipping options
- Add support for `exclude` entries in the scan settings BOM model
- Add ability to change default API URL during compilation: `make build DEFAULT_API_URL=api.something.else`

### Changed
- Allow component filters without requiring a `purl`, enabling path-only filter entries to be serialized cleanly

### Fixed
- Fix component replacement displaying incorrect purl and component name when the scanner pre-applies the replacement
- Fix the result view not scrolling into view when navigating with keyboard shortcuts
- Fix action bar buttons not being interactive on completed results that need to be re-evaluated

## [0.11.0] 2026-01-26
### Added
- Folder actions: Include, dismiss, replace, or skip all files within a folder in a single action
Expand Down Expand Up @@ -247,3 +263,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[0.9.9]: https://github.com/scanoss/scanoss.cc/compare/v0.9.8...v0.9.9
[0.10.0]: https://github.com/scanoss/scanoss.cc/compare/v0.9.9...v0.10.0
[0.11.0]: https://github.com/scanoss/scanoss.cc/compare/v0.10.0...v0.11.0
[0.12.0]: https://github.com/scanoss/scanoss.cc/compare/v0.11.0...v0.12.0
13 changes: 7 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ SCRIPTS_DIR = scripts
FRONTEND_DIR = frontend
ASSETS_DIR = assets
APP_BUNDLE = "$(BUILD_DIR)/bin/$(APP_NAME).app"
DEFAULT_API_URL = "https://api.osskb.org"

export GOTOOLCHAIN=go1.23.0

Expand Down Expand Up @@ -48,11 +49,11 @@ lint-fix: ## Run local instance of Go linting across the code base including aut

run: cp_assets ## Runs the application in development mode
$(eval APPARGS := $(ARGS))
@wails dev -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION)" $(if $(strip $(APPARGS)),-appargs "--debug $(APPARGS)")
@wails dev -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION) -X github.com/scanoss/scanoss.cc/internal/config.DefaultAPIURL=$(DEFAULT_API_URL)" $(if $(strip $(APPARGS)),-appargs "--debug $(APPARGS)")

run_webkit41: cp_assets ## Runs the application in development mode for Ubuntu 24.04+/Debian 13+
$(eval APPARGS := $(ARGS))
@wails dev -tags webkit2_41 -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION)" $(if $(strip $(APPARGS)),-appargs "--debug $(APPARGS)")
@wails dev -tags webkit2_41 -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION) -X github.com/scanoss/scanoss.cc/internal/config.DefaultAPIURL=$(DEFAULT_API_URL)" $(if $(strip $(APPARGS)),-appargs "--debug $(APPARGS)")

npm: ## Install NPM dependencies for the frontend
@echo "Running npm install for frontend..."
Expand All @@ -66,17 +67,17 @@ cp_assets: ## Copy the necessary assets to the build folder

build: clean cp_assets ## Build the application image for the current platform
@echo "Building application image..."
@wails build -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION)"
@wails build -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION) -X github.com/scanoss/scanoss.cc/internal/config.DefaultAPIURL=$(DEFAULT_API_URL)"

binary: cp_assets ## Build application binary only (no package)
@echo "Build application binary only..."
@wails build -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION)" --nopackage
@wails build -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION) -X github.com/scanoss/scanoss.cc/internal/config.DefaultAPIURL=$(DEFAULT_API_URL)" --nopackage

build_macos: clean cp_assets ## Build the application image for macOS
@echo "Building application image for macOS..."
@wails build -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION)" -platform darwin/universal -o "$(APP_NAME)"
@wails build -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION) -X github.com/scanoss/scanoss.cc/internal/config.DefaultAPIURL=$(DEFAULT_API_URL)" -platform darwin/universal -o "$(APP_NAME)"
@echo "Build completed. Result: $(APP_BUNDLE)"

build_webkit41: clean cp_assets ## Build the application image for Ubuntu 24.04+/Debian 13+ (webkit 4.1)
@echo "Building application image with webkit2_41 tags..."
@wails build -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION)" -tags webkit2_41
@wails build -ldflags "-X github.com/scanoss/scanoss.cc/backend/entities.AppVersion=$(VERSION) -X github.com/scanoss/scanoss.cc/internal/config.DefaultAPIURL=$(DEFAULT_API_URL)" -tags webkit2_41
3 changes: 2 additions & 1 deletion backend/entities/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,14 @@ const (
Include FilterAction = "include"
Remove FilterAction = "remove"
Replace FilterAction = "replace"
Restore FilterAction = "restore"
)

type ComponentFilterDTO struct {
Path string `json:"path,omitempty"`
Purl string `json:"purl,omitempty"`
Usage string `json:"usage,omitempty"`
Action FilterAction `json:"action" validate:"required,eq=include|eq=remove|eq=replace"`
Action FilterAction `json:"action" validate:"required,eq=include|eq=remove|eq=replace|eq=restore"`
Comment string `json:"comment,omitempty"`
ReplaceWith string `json:"replace_with,omitempty" validate:"omitempty,valid-purl"`
License string `json:"license,omitempty"`
Expand Down
4 changes: 4 additions & 0 deletions backend/entities/keyboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ const (
ActionReplaceFolder Action = "replaceFolder"
ActionReplaceComponent Action = "replaceComponent"

// Restore action (undo decision on completed result)
ActionRestoreFile Action = "restoreFile"

// Skip action (Scan settings) - always opens modal
ActionSkipFile Action = "skipFile"
ActionSkipFolder Action = "skipFolder"
Expand Down Expand Up @@ -106,6 +109,7 @@ var AllShortcutActions = []struct {
{ActionReplaceFile, "ReplaceFile"},
{ActionReplaceFolder, "ReplaceFolder"},
{ActionReplaceComponent, "ReplaceComponent"},
{ActionRestoreFile, "RestoreFile"},
{ActionSkipFile, "SkipFile"},
{ActionSkipFolder, "SkipFolder"},
{ActionSkipExtension, "SkipExtension"},
Expand Down
1 change: 1 addition & 0 deletions backend/entities/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ type ResultDTO struct {
Comment string `json:"comment,omitempty"`
DetectedPurl string `json:"detected_purl,omitempty"`
DetectedPurlUrl string `json:"detected_purl_url,omitempty"`
DetectedName string `json:"detected_name,omitempty"`
ConcludedPurl string `json:"concluded_purl,omitempty"`
ConcludedPurlUrl string `json:"concluded_purl_url,omitempty"`
ConcludedName string `json:"concluded_name,omitempty"`
Expand Down
26 changes: 23 additions & 3 deletions backend/entities/scanoss_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ type SettingsFile struct {
}

type ScanossSettingsSchema struct {
Skip SkipSettings `json:"skip,omitempty"`
Skip SkipSettings `json:"skip,omitempty"`
FileSnippet FileSnippetSettings `json:"file_snippet,omitempty"`
}

type SkipSettings struct {
Expand All @@ -70,15 +71,26 @@ type SizesSkipSettings struct {
Max int `json:"max,omitempty"`
}

type FileSnippetSettings struct {
RankingEnabled bool `json:"ranking_enabled,omitempty"`
RankingThreshold *int `json:"ranking_threshold,omitempty"`
MinSnippetHits int `json:"min_snippet_hits,omitempty"`
MinSnippetLines int `json:"min_snippet_lines,omitempty"`
HonourFileExts bool `json:"honour_file_exts,omitempty"`
SkipHeaders bool `json:"skip_headers,omitempty"`
SkipHeadersLimit int `json:"skip_headers_limit,omitempty"`
}

type Bom struct {
Include []ComponentFilter `json:"include,omitempty"`
Remove []ComponentFilter `json:"remove,omitempty"`
Replace []ComponentFilter `json:"replace,omitempty"`
Exclude []ComponentFilter `json:"exclude,omitempty"`
}

type ComponentFilter struct {
Path string `json:"path,omitempty"`
Purl string `json:"purl"`
Purl string `json:"purl,omitempty"`
Usage ComponentFilterUsage `json:"usage,omitempty"`
Comment string `json:"comment,omitempty"`
ReplaceWith string `json:"replace_with,omitempty"`
Expand Down Expand Up @@ -159,11 +171,19 @@ func (cf ComponentFilter) MatchesPath(path string) bool {

// MatchesAnyPurl checks if the purl constraint is satisfied.
// Empty purl means no constraint (always satisfied).
// For replace entries, also matches if the result's purl equals ReplaceWith,
// since the scanner may have already applied the replacement.
func (cf ComponentFilter) MatchesAnyPurl(purls []string) bool {
if cf.Purl == "" {
return true // No constraint
}
return slices.Contains(purls, cf.Purl)
if slices.Contains(purls, cf.Purl) {
return true
}
if cf.ReplaceWith != "" && slices.Contains(purls, cf.ReplaceWith) {
return true
}
return false
}

// AppliesTo checks if all filter constraints are satisfied by the result.
Expand Down
18 changes: 18 additions & 0 deletions backend/entities/scanoss_settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,24 @@ func TestComponentFilter_AppliesTo(t *testing.T) {
result: Result{Path: "src/file.js", Purl: nil},
expected: false,
},
{
name: "replace filter applies when result purl matches replace_with",
filter: ComponentFilter{Path: "src/file.js", Purl: "pkg:npm/original@1.0.0", ReplaceWith: "pkg:npm/lodash@1.0.0"},
result: Result{Path: "src/file.js", Purl: &purl},
expected: true,
},
{
name: "replace filter applies when result purl matches original purl",
filter: ComponentFilter{Path: "src/file.js", Purl: "pkg:npm/lodash@1.0.0", ReplaceWith: "pkg:npm/other@2.0.0"},
result: Result{Path: "src/file.js", Purl: &purl},
expected: true,
},
{
name: "replace filter does not apply when neither purl nor replace_with match",
filter: ComponentFilter{Path: "src/file.js", Purl: "pkg:npm/original@1.0.0", ReplaceWith: "pkg:npm/other@2.0.0"},
result: Result{Path: "src/file.js", Purl: &purl},
expected: false,
},
}

for _, tt := range tests {
Expand Down
4 changes: 2 additions & 2 deletions backend/entities/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ package entities

import "time"

// Gets updated build time using -ldflags
// AppVersion Gets updated build time using -ldflags.
var AppVersion = ""

// UpdateInfo contains information about an available update
// UpdateInfo contains information about an available update.
type UpdateInfo struct {
Version string `json:"version"`
DownloadURL string `json:"download_url"`
Expand Down
65 changes: 29 additions & 36 deletions backend/mappers/result_mapper_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,29 @@ func (m *ResultMapperImpl) MapToResultDTO(result entities.Result) entities.Resul
detectedPurl = (*result.Purl)[0]
}

// If the scanner already applied the replacement, the detected purl will
// equal the replace_with value. In that case, show the original purl from
// the BOM entry so the display shows "original -> replacement".
scannerPreAppliedReplacement := bomEntry.ReplaceWith != "" && detectedPurl == bomEntry.ReplaceWith && bomEntry.Purl != ""
if scannerPreAppliedReplacement {
detectedPurl = bomEntry.Purl
}

var detectedName string
if scannerPreAppliedReplacement {
detectedName = m.componentNameFromPurl(bomEntry.Purl)
} else {
detectedName = result.ComponentName
}

dto := entities.ResultDTO{
MatchType: entities.MatchType(result.MatchType),
Path: result.Path,
DetectedPurl: detectedPurl,
DetectedPurlUrl: m.mapDetectedPurlUrl(result),
DetectedPurlUrl: m.mapPurlUrl(detectedPurl),
DetectedName: detectedName,
ConcludedPurl: bomEntry.ReplaceWith,
ConcludedPurlUrl: m.mapConcludedPurlUrl(result),
ConcludedPurlUrl: m.mapPurlUrl(bomEntry.ReplaceWith),
ConcludedName: m.mapConcludedName(result),
WorkflowState: m.mapWorkflowState(result),
FilterConfig: m.mapFilterConfig(result),
Expand All @@ -91,9 +107,6 @@ func (m *ResultMapperImpl) MapToResultDTO(result entities.Result) entities.Resul
return dto
}

func (m *ResultMapperImpl) mapConcludedPurl(result entities.Result) string {
return m.scanossSettings.SettingsFile.GetBomEntryFromResult(result).ReplaceWith
}

func (m *ResultMapperImpl) MapToResultDTOList(results []entities.Result) []entities.ResultDTO {
output := make([]entities.ResultDTO, len(results))
Expand Down Expand Up @@ -128,28 +141,6 @@ func (m *ResultMapperImpl) mapFilterConfig(result entities.Result) entities.Filt
return m.scanossSettings.SettingsFile.GetResultFilterConfig(result)
}

func (m *ResultMapperImpl) mapConcludedPurlUrl(result entities.Result) string {
concludedPurl := m.mapConcludedPurl(result)
if concludedPurl == "" {
return ""
}

purlObject, err := purlutils.PurlFromString(concludedPurl)
if err != nil {
log.Error().Err(err).Msg("Error parsing concluded purl")
return ""
}

// Workaround for github purls until purlutils is updated
purlName := purlObject.Name
if purlObject.Type == "github" && purlObject.Namespace != "" {
purlName = fmt.Sprintf("%s/%s", purlObject.Namespace, purlObject.Name)
}

return m.getProjectURL(concludedPurl, func() (string, error) {
return purlutils.ProjectUrl(purlName, purlObject.Type)
})
}

func (m *ResultMapperImpl) getProjectURL(purl string, compute func() (string, error)) string {
if cached, ok := purlCache.Load(purl); ok {
Expand All @@ -164,35 +155,37 @@ func (m *ResultMapperImpl) getProjectURL(purl string, compute func() (string, er
return url
}

func (m *ResultMapperImpl) mapDetectedPurlUrl(result entities.Result) string {
if result.Purl == nil || len(*result.Purl) == 0 {
func (m *ResultMapperImpl) mapPurlUrl(purl string) string {
if purl == "" {
return ""
}

detectedPurl := (*result.Purl)[0]

purlObject, err := purlutils.PurlFromString(detectedPurl)
purlObject, err := purlutils.PurlFromString(purl)
if err != nil {
log.Error().Err(err).Msg("Error parsing detected purl")
log.Error().Err(err).Msg("Error parsing purl")
return ""
}
purlName := purlObject.Name
if purlObject.Type == "github" && purlObject.Namespace != "" {
purlName = fmt.Sprintf("%s/%s", purlObject.Namespace, purlObject.Name)
}

return m.getProjectURL(detectedPurl, func() (string, error) {
return m.getProjectURL(purl, func() (string, error) {
return purlutils.ProjectUrl(purlName, purlObject.Type)
})
}

func (m *ResultMapperImpl) mapConcludedName(result entities.Result) string {
replacedPurl := m.scanossSettings.SettingsFile.GetBomEntryFromResult(result).ReplaceWith
if replacedPurl == "" {
return m.componentNameFromPurl(replacedPurl)
}

func (m *ResultMapperImpl) componentNameFromPurl(purl string) string {
if purl == "" {
return ""
}

purlName, err := purlutils.PurlNameFromString(replacedPurl)
purlName, err := purlutils.PurlNameFromString(purl)
if err != nil {
log.Error().Err(err).Msg("Error getting component name from purl")
return ""
Expand Down
46 changes: 46 additions & 0 deletions backend/repository/mocks/mock_ScanossSettingsRepository.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/repository/scanoss_settings_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type ScanossSettingsRepository interface {
Read() (entities.SettingsFile, error)
HasUnsavedChanges() (bool, error)
AddBomEntry(newEntry entities.ComponentFilter, filterAction string) error
RemoveBomEntry(entry entities.ComponentFilter) error
ClearAllFilters() error
GetSettings() *entities.SettingsFile
GetDeclaredPurls() []string
Expand Down
Loading
Loading