diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ea617e..c7fabfd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
+## [1.6.0] - 2025-12-26
+### Added
+- Added scanoss.json scanning config support.
+- Added new server-side configuration parameters for scanning tune-up.
+ - rankingAllowed
+ - rankingEnabled
+ - rankingThreshold
+ - minSnippetHits
+ - minSnippetLines
+ - honourFileExts
+
## [1.5.2] - 2025-11-07
### Added
- Added Custom Contents URL support (`SCANOSS_FILE_CONTENTS_URL`)
@@ -161,3 +172,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[1.5.0]: https://github.com/scanoss/api.go/compare/v1.4.5...v1.5.0
[1.5.1]: https://github.com/scanoss/api.go/compare/v1.5.0...v1.5.1
[1.5.2]: https://github.com/scanoss/api.go/compare/v1.5.1...v1.5.2
+[1.6.0]: https://github.com/scanoss/api.go/compare/v1.5.2...v1.6.0
diff --git a/Makefile b/Makefile
index ef98526..c7ce9be 100644
--- a/Makefile
+++ b/Makefile
@@ -33,10 +33,18 @@ unit_test: ## Run all unit tests in the pkg folder
@echo "Running unit test framework..."
go test -v ./pkg/...
+unit_test_cover: ## Run all unit tests in the pkg folder
+ @echo "Running unit test framework with coverage..."
+ go test -cover ./pkg/...
+
int_test: clean_testcache ## Run all integration tests in the tests folder
@echo "Running integration test framework..."
go test -v ./tests
+int_test_cover: clean_testcache ## Run all integration tests in the tests folder
+ @echo "Running integration test framework..."
+ go test -cover -v ./tests
+
lint_local_clean: ## Cleanup the local cache from the linter
@echo "Cleaning linter cache..."
golangci-lint cache clean
@@ -67,6 +75,13 @@ e2e_test: docker_build_test clean_testcache ## Run end to end integration tests
${DOCKER} compose exec -T http go test -v -tags="integration e2e" ./tests
${DOCKER} compose down
+e2e_test_cover: docker_build_test clean_testcache ## Run end to end integration tests using Docker
+ @echo "Running End-to-End tests..."
+ ${DOCKER} compose down
+ ${DOCKER} compose up -d
+ ${DOCKER} compose exec -T http go test -cover -v -tags="integration e2e" ./tests
+ ${DOCKER} compose down
+
ghcr_build: version ## Build GitHub container image
@echo "Building GHCR container image..."
${DOCKER} build --no-cache -t $(GHCR_FULLNAME) --platform linux/amd64 .
diff --git a/config/app-config-dev.json b/config/app-config-dev.json
index 3d00b81..9dcf474 100644
--- a/config/app-config-dev.json
+++ b/config/app-config-dev.json
@@ -9,6 +9,13 @@
"Scanning": {
"ScanBinary": "./test-support/scanoss.sh",
"ScanningURL": "http://localhost:5443",
- "TmpFileDelete": true
+ "TmpFileDelete": true,
+ "ScanKbName": "oss",
+ "RankingAllowed": true,
+ "RankingEnabled": false,
+ "RankingThreshold": 0,
+ "MinSnippetHits": 0,
+ "MinSnippetLines": 0,
+ "HonourFileExts": true
}
}
diff --git a/config/app-config-prod.json b/config/app-config-prod.json
index dff4f89..ab485d2 100644
--- a/config/app-config-prod.json
+++ b/config/app-config-prod.json
@@ -32,7 +32,13 @@
"KeepFailedWfps": true,
"HPSMEnabled": true,
"FileContents": true,
- "LoadKbDetails": true
+ "LoadKbDetails": true,
+ "RankingAllowed": true,
+ "RankingEnabled": false,
+ "RankingThreshold": 0,
+ "MinSnippetHits": 0,
+ "MinSnippetLines": 0,
+ "HonourFileExts": true
},
"TLS": {
"CertFile": "",
diff --git a/go.mod b/go.mod
index 25a6413..a5c09ce 100644
--- a/go.mod
+++ b/go.mod
@@ -11,21 +11,22 @@ require (
github.com/scanoss/zap-logging-helper v0.4.0
github.com/stretchr/testify v1.11.1
github.com/wlynxg/chardet v1.0.4
- go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0
- go.opentelemetry.io/otel v1.38.0
- go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
- go.opentelemetry.io/otel/metric v1.38.0
- go.opentelemetry.io/otel/sdk v1.38.0
- go.opentelemetry.io/otel/sdk/metric v1.38.0
- go.opentelemetry.io/otel/trace v1.38.0
- go.uber.org/zap v1.27.0
+ go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.64.0
+ go.opentelemetry.io/otel v1.39.0
+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0
+ go.opentelemetry.io/otel/metric v1.39.0
+ go.opentelemetry.io/otel/sdk v1.39.0
+ go.opentelemetry.io/otel/sdk/metric v1.39.0
+ go.opentelemetry.io/otel/trace v1.39.0
+ go.uber.org/zap v1.27.1
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -33,22 +34,23 @@ require (
github.com/golobby/cast v1.3.3 // indirect
github.com/golobby/dotenv v1.3.2 // indirect
github.com/golobby/env/v2 v2.2.4 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
- github.com/phuslu/iploc v1.0.20250901 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // indirect
+ github.com/hashicorp/go-version v1.8.0 // indirect
+ github.com/phuslu/iploc v1.0.20260115 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
- go.opentelemetry.io/auto/sdk v1.1.0 // indirect
- go.opentelemetry.io/proto/otlp v1.7.1 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/net v0.43.0 // indirect
- golang.org/x/sys v0.35.0 // indirect
- golang.org/x/text v0.28.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
- google.golang.org/grpc v1.75.1 // indirect
- google.golang.org/protobuf v1.36.9 // indirect
+ golang.org/x/net v0.47.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
+ google.golang.org/grpc v1.78.0 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 2e180c7..5bcd4de 100644
--- a/go.sum
+++ b/go.sum
@@ -3,6 +3,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -35,6 +37,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI=
+github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
+github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/jpillora/ipfilter v1.2.9 h1:vjjcI1JpxZ6HvIj1MZfomhrfzXW/67QNdE449ZZfon8=
github.com/jpillora/ipfilter v1.2.9/go.mod h1:QUYQLXQU0myCdxZVbYBZ5+An/qtSB2m1OBRiwqTa9pk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -49,6 +55,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/phuslu/iploc v1.0.20230201/go.mod h1:gsgExGWldwv1AEzZm+Ki9/vGfyjkL33pbSr9HGpt2Xg=
github.com/phuslu/iploc v1.0.20250901 h1:zI/aYfKpvaL3xaErWp5xIIdLG5UIVAGjBt5dAoT42PM=
github.com/phuslu/iploc v1.0.20250901/go.mod h1:VZqAWoi2A80YPvfk1AizLGHavNIG9nhBC8d87D/SeVs=
+github.com/phuslu/iploc v1.0.20260115 h1:DSo9u0GSVkNUXq1ZRYpe50kEjmyyWTkcNcSnUbeT1TU=
+github.com/phuslu/iploc v1.0.20260115/go.mod h1:VZqAWoi2A80YPvfk1AizLGHavNIG9nhBC8d87D/SeVs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -77,28 +85,51 @@ github.com/wlynxg/chardet v1.0.4 h1:hkI71Dx8v3RiAz3XKV5lJEh9QfKo7xXKUmYJQeIMlpo=
github.com/wlynxg/chardet v1.0.4/go.mod h1:HLQMNsa0w4MkH2e7waQaFD+Yh85riFFTLhFtP8fsdbQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0 h1:rATLgFjv0P9qyXQR/aChJ6JVbMtXOQjt49GgT36cBbk=
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0/go.mod h1:34csimR1lUhdT5HH4Rii9aKPrvBcnFRwxLwcevsU+Kk=
+go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.64.0 h1:vwZaYp+EEiPUQD1rYKPT0vLfGD7XMv2WypO/59ySpwM=
+go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.64.0/go.mod h1:D96L6/izMrfhIlFm1sFiyEC8zVyMcDzC8dwqUoTmGT8=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
+go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
+go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
+go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
+go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
+go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
+go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
+go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
+go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
+go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
+go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
@@ -108,22 +139,40 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
+go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 h1:d8Nakh1G+ur7+P3GcMjpRDEkoLUcLW2iU92XVqR+XMQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw=
+google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 h1:X9z6obt+cWRX8XjDVOn+SZWhWe5kZHm46TThU9j+jss=
+google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
+google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
+google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
diff --git a/pkg/config/server_config.go b/pkg/config/server_config.go
index e5a6ffd..b4f9f4f 100644
--- a/pkg/config/server_config.go
+++ b/pkg/config/server_config.go
@@ -70,6 +70,14 @@ type ServerConfig struct {
FileContents bool `env:"SCANOSS_FILE_CONTENTS"` // Show matched file URL in scan results (default true)
FileContentsURL string `env:"SCANOSS_FILE_CONTENTS_URL"` // Explicit file contents URL to use for the engine
LoadKbDetails bool `env:"SCANOSS_LOAD_KB_DETAILS"` // Load the version of the KB into the service for reporting
+ // component selection
+ RankingAllowed bool `env:"SCANOSS_RANKING_ALLOWED"` // Allow ranking to be used in scan results
+ RankingEnabled bool `env:"SCANOSS_RANKING_ENABLED"` // Enable ranking in scan results
+ RankingThreshold int `env:"SCANOSS_RANKING_THRESHOLD"` // Ranking threshold to use
+ // snippet matching
+ MinSnippetHits int `env:"SCANOSS_MIN_SNIPPET_HITS"` // Minimum snippet hits to consider a snippet match
+ MinSnippetLines int `env:"SCANOSS_MIN_SNIPPET_LINES"` // Minimum snippet lines to consider a snippet match
+ HonourFileExts bool `env:"SCANOSS_HONOUR_FILE_EXTS"` // Honour file extensions to filter snippet matches
}
TLS struct {
CertFile string `env:"SCAN_TLS_CERT"` // TLS Certificate
@@ -120,6 +128,14 @@ func setServerConfigDefaults(cfg *ServerConfig) {
cfg.Telemetry.OltpExporter = "0.0.0.0:4317" // Default OTEL OLTP gRPC Exporter endpoint
cfg.Scanning.FileContents = true // Matched File URL response enabled (true) by default
cfg.Scanning.LoadKbDetails = true // Load the KB details on a scheduler
+ // component selection
+ cfg.Scanning.RankingAllowed = true // Allow ranking to be used in scan results
+ cfg.Scanning.RankingEnabled = false // Disable ranking in scan results by default
+ cfg.Scanning.RankingThreshold = 0 // Ranking threshold default (everything is accepted)
+ // snippet matching
+ cfg.Scanning.MinSnippetHits = 0 // Lets the engine decide on minimum snippet hits based on the file total lines
+ cfg.Scanning.MinSnippetLines = 0 // Lets the engine decide on minimum snippet hits on the file total lines
+ cfg.Scanning.HonourFileExts = true
}
// LoadFile loads the specified file and returns its contents in a string array.
diff --git a/pkg/service/kb_details.go b/pkg/service/kb_details.go
index ded7890..5fc4cdf 100644
--- a/pkg/service/kb_details.go
+++ b/pkg/service/kb_details.go
@@ -24,7 +24,9 @@ import (
"time"
"github.com/go-co-op/gocron"
+ "github.com/hashicorp/go-version"
zlog "github.com/scanoss/zap-logging-helper/pkg/logger"
+ "go.uber.org/zap"
)
// Structure for parsing KB & Engine version from scan response.
@@ -42,6 +44,29 @@ type matchStructure []struct {
var kbDetails string // KB Details JSON string
var engineVersion string // Version of the engine in use
+// validateEngineVersion validates that the current engine version meets the minimum requirement.
+// Logs a critical error if the version is below minimum, or an info message if it meets the requirement.
+func validateEngineVersion(zs *zap.SugaredLogger, currentEngineVersion, minEngineVersion string) {
+ if minEngineVersion == "" || currentEngineVersion == "unknown" || currentEngineVersion == "" {
+ return
+ }
+ currentVersion, err := version.NewVersion(currentEngineVersion)
+ if err != nil {
+ zs.Errorf("Failed to parse current engine version '%s': %v", currentEngineVersion, err)
+ return
+ }
+ minVersion, err := version.NewVersion(minEngineVersion)
+ if err != nil {
+ zs.Errorf("Failed to parse minimum engine version '%s': %v", minEngineVersion, err)
+ return
+ }
+ if currentVersion.LessThan(minVersion) {
+ zs.Errorf("Engine version '%s' is below the minimum required version '%s'. Some features may not work as expected.", currentEngineVersion, minEngineVersion)
+ } else {
+ zs.Infof("Engine version '%s' meets minimum requirement '%s'", currentEngineVersion, minEngineVersion)
+ }
+}
+
// SetupKBDetailsCron sets up a background cron to update the KB version once an hour.
func (s APIService) SetupKBDetailsCron() {
if s.config.Scanning.LoadKbDetails {
@@ -79,13 +104,14 @@ func (s APIService) KBDetails(w http.ResponseWriter, r *http.Request) {
// loadKBDetails attempts to scan a file to load the latest KB details from the server.
func (s APIService) loadKBDetails() {
- zs := sugaredLogger(context.TODO()) // Setup logger without context
+ zs := sugaredLogger(context.TODO()) // Set up a logger without context
zs.Debugf("Loading latest KB details...")
if len(engineVersion) == 0 {
engineVersion = "unknown"
}
// Load a random (hopefully non-existent) file match to extract the KB version details
- result, err := s.scanWfp("file=7c53a2de7dfeaa20d057db98468d6670,2321,path/to/dummy/file.txt", "", "", "", "", zs)
+ emptyConfig := DefaultScanningServiceConfig(s.config)
+ result, err := s.scanWfp("file=7c53a2de7dfeaa20d057db98468d6670,2321,path/to/dummy/file.txt", "", emptyConfig, zs)
if err != nil {
zs.Warnf("Failed to detect KB version from eninge: %v", err)
return
@@ -121,6 +147,7 @@ func (s APIService) loadKBDetails() {
if len(ms) > 0 {
kbDetails = fmt.Sprintf(`{"kb_version": { "monthly": "%v", "daily": "%v"}}`, ms[0].Server.KbVersion.Monthly, ms[0].Server.KbVersion.Daily)
engineVersion = ms[0].Server.Version
+ validateEngineVersion(zs, engineVersion, minEngineVersion)
}
}
}
diff --git a/pkg/service/kb_details_test.go b/pkg/service/kb_details_test.go
index a5da810..b686347 100644
--- a/pkg/service/kb_details_test.go
+++ b/pkg/service/kb_details_test.go
@@ -83,3 +83,32 @@ func TestKBDetails(t *testing.T) {
myConfig.Scanning.ScanBinary = "../path/to/does-not-exist.sh"
apiService.loadKBDetails()
}
+
+// TestEngineVersionBelowMinimum tests that a critical error is logged when engine version is below minimum.
+func TestEngineVersionBelowMinimum(t *testing.T) {
+ err := zlog.NewSugaredDevLogger()
+ if err != nil {
+ t.Fatalf("an error '%s' was not expected when opening a sugared logger", err)
+ }
+ defer zlog.SyncZap()
+
+ myConfig := setupConfig(t)
+ myConfig.App.Trace = true
+ myConfig.Scanning.LoadKbDetails = true
+
+ apiService := NewAPIService(myConfig)
+
+ // Simulate engine version below minimum
+ engineVersion = "5.4.0"
+
+ // Setup cron which will call loadKBDetails
+ apiService.SetupKBDetailsCron()
+
+ // Wait for the cron to execute
+ time.Sleep(time.Duration(3) * time.Second)
+
+ // The critical error should have been logged
+ // (we can't easily assert on log output without capturing it,
+ // but the function will execute and log the error)
+ fmt.Println("Engine version validation test completed - check logs for CRITICAL error")
+}
diff --git a/pkg/service/scanning_service.go b/pkg/service/scanning_service.go
index 5880ae6..4eb0256 100644
--- a/pkg/service/scanning_service.go
+++ b/pkg/service/scanning_service.go
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
- * Copyright (C) 2018-2023 SCANOSS.COM
+ * Copyright (C) 2018-2025 SCANOSS.COM
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@ package service
import (
"bytes"
"context"
+ "encoding/base64"
"fmt"
"net/http"
"os"
@@ -36,6 +37,12 @@ import (
"go.uber.org/zap"
)
+const (
+ sbomIdentify = "identify" // SBOM type to identify components
+ sbomBlackList = "blacklist" // SBOM type to blacklist components
+ minEngineVersion = "5.4.20" // Minimum required engine version
+)
+
var fileRegex = regexp.MustCompile(`^\w+,(\d+),.+`) // regex to parse file size from request
// ScanDirect handles WFP scanning requests from a client.
@@ -86,16 +93,21 @@ func (s APIService) scanDirect(w http.ResponseWriter, r *http.Request, zs *zap.S
setSpanError(span, "No WFP contents supplied")
return 0
}
- flags, scanType, sbom, dbName := s.getFlags(r, zs)
+ scanConfig, err := s.getConfigFromRequest(r, zs)
+ if err != nil {
+ http.Error(w, "ERROR invalid scanning configuration", http.StatusBadRequest)
+ setSpanError(span, "Invalid scanning configuration.")
+ return 0
+ }
// Check if we have an SBOM (and type) supplied
var sbomFilename string
- if len(sbom) > 0 && len(scanType) > 0 {
- if scanType != "identify" && scanType != "blacklist" { // Make sure we have a valid SBOM scan type
- zs.Errorf("Invalid SBOM type: %v", scanType)
+ if len(scanConfig.sbomFile) > 0 && len(scanConfig.sbomType) > 0 {
+ if scanConfig.sbomType != sbomIdentify && scanConfig.sbomType != sbomBlackList { // Make sure we have a valid SBOM scan type
+ zs.Errorf("Invalid SBOM type: %v", scanConfig.sbomType)
http.Error(w, "ERROR invalid SBOM 'type' supplied", http.StatusBadRequest)
return 0
}
- tempFile, err := s.writeSbomFile(sbom, zs)
+ tempFile, err := s.writeSbomFile(scanConfig.sbomFile, zs)
if err != nil {
http.Error(w, "ERROR engine scan failed", http.StatusInternalServerError)
return 0
@@ -104,7 +116,7 @@ func (s APIService) scanDirect(w http.ResponseWriter, r *http.Request, zs *zap.S
defer removeFile(tempFile, zs)
}
sbomFilename = tempFile.Name() // Save the SBOM filename
- zs.Debugf("Stored SBOM (%v) in %v", scanType, sbomFilename)
+ zs.Debugf("Stored SBOM (%v) in %v", scanConfig.sbomType, sbomFilename)
}
wfps := strings.Split(string(contentsTrimmed), "file=")
wfpCount := int64(len(wfps) - 1) // First entry in the array is empty (hence the -1)
@@ -121,9 +133,9 @@ func (s APIService) scanDirect(w http.ResponseWriter, r *http.Request, zs *zap.S
s.countScanSize(wfps, wfpCount, zs, context, span)
// Only one worker selected, so send the whole WFP in a single command
if s.config.Scanning.Workers <= 1 {
- s.singleScan(string(contentsTrimmed), flags, scanType, sbomFilename, dbName, zs, w)
+ s.singleScan(string(contentsTrimmed), sbomFilename, scanConfig, zs, w)
} else {
- s.scanThreaded(wfps, int(wfpCount), flags, scanType, sbomFilename, dbName, zs, w, span)
+ s.scanThreaded(wfps, int(wfpCount), sbomFilename, scanConfig, zs, w, span)
}
return wfpCount
}
@@ -154,29 +166,45 @@ func (s APIService) countScanSize(wfps []string, wfpCount int64, zs *zap.Sugared
zs.Infof("Need to scan %v files of size %v", wfpCount, sizeCount)
}
-// getFlags extracts the form values from a request returns the flags, scan type, and sbom data if detected.
-func (s APIService) getFlags(r *http.Request, zs *zap.SugaredLogger) (string, string, string, string) {
- flags := strings.TrimSpace(r.FormValue("flags")) // Check form for Scanning flags
+// getConfigFromRequest extracts the form values from a request and returns the scanning configuration.
+func (s APIService) getConfigFromRequest(r *http.Request, zs *zap.SugaredLogger) (ScanningServiceConfig, error) {
+ flags := strings.TrimSpace(r.FormValue("flags")) // Check form for scanning flags
scanType := strings.TrimSpace(r.FormValue("type")) // Check form for SBOM type
sbom := strings.TrimSpace(r.FormValue("assets")) // Check form for SBOM contents
dbName := strings.TrimSpace(r.FormValue("db_name")) // Check form for db name
- // TODO is it necessary to check the header also for these values?
+ // Fall back to headers if form values are empty
if len(flags) == 0 {
- flags = strings.TrimSpace(r.Header.Get("flags")) // Check header for Scanning flags
+ flags = strings.TrimSpace(r.Header.Get("flags"))
}
if len(scanType) == 0 {
- scanType = strings.TrimSpace(r.Header.Get("type")) // Check header for SBOM type
+ scanType = strings.TrimSpace(r.Header.Get("type"))
}
if len(sbom) == 0 {
- sbom = strings.TrimSpace(r.Header.Get("assets")) // Check header for SBOM contents
+ sbom = strings.TrimSpace(r.Header.Get("assets"))
}
if len(dbName) == 0 {
- dbName = strings.TrimSpace(r.Header.Get("db_name")) // Check header for SBOM contents
+ dbName = strings.TrimSpace(r.Header.Get("db_name"))
}
+ scanSettings := strings.TrimSpace(r.Header.Get("scanoss-settings")) // Check the header for scan settings
if s.config.App.Trace {
- zs.Debugf("Header: %v, Form: %v, flags: %v, type: %v, assets: %v, db_name %v", r.Header, r.Form, flags, scanType, sbom, dbName)
+ zs.Debugf("Header: %v, Form: %v, flags: %v, type: %v, assets: %v, db_name: %v, scanSettings: %v",
+ r.Header, r.Form, flags, scanType, sbom, dbName, scanSettings)
+ }
+ // Create default configuration from server config
+ scanConfig := DefaultScanningServiceConfig(s.config)
+ // Decode scan settings from base64 if provided
+ var decoded []byte
+ if len(scanSettings) > 0 {
+ var err error
+ decoded, err = base64.StdEncoding.DecodeString(scanSettings)
+ if err != nil {
+ zs.Errorf("Error decoding scan settings from base64: %v", err)
+ return scanConfig, fmt.Errorf("error decoding scan settings from base64: %v", err)
+ } else if s.config.App.Trace {
+ zs.Debugf("Decoded scan settings: %s", string(decoded))
+ }
}
- return flags, scanType, sbom, dbName
+ return UpdateScanningServiceConfigDTO(zs, &scanConfig, flags, scanType, sbom, dbName, decoded)
}
// writeSbomFile writes the given string into an SBOM temporary file.
@@ -196,9 +224,9 @@ func (s APIService) writeSbomFile(sbom string, zs *zap.SugaredLogger) (*os.File,
}
// singleScan runs a scan of the WFP in a single thread.
-func (s APIService) singleScan(wfp, flags, sbomType, sbomFile, dbName string, zs *zap.SugaredLogger, w http.ResponseWriter) {
+func (s APIService) singleScan(wfp, sbomFile string, config ScanningServiceConfig, zs *zap.SugaredLogger, w http.ResponseWriter) {
zs.Debugf("Single threaded scan...")
- result, err := s.scanWfp(wfp, flags, sbomType, sbomFile, dbName, zs)
+ result, err := s.scanWfp(wfp, sbomFile, config, zs)
if err != nil {
zs.Errorf("Engine scan failed: %v", err)
http.Error(w, "ERROR engine scan failed", http.StatusInternalServerError)
@@ -216,7 +244,7 @@ func (s APIService) singleScan(wfp, flags, sbomType, sbomFile, dbName string, zs
}
// scanThreaded scan the given WFPs in multiple threads.
-func (s APIService) scanThreaded(wfps []string, wfpCount int, flags, sbomType, sbomFile, dbName string, zs *zap.SugaredLogger, w http.ResponseWriter, span oteltrace.Span) {
+func (s APIService) scanThreaded(wfps []string, wfpCount int, sbomFile string, config ScanningServiceConfig, zs *zap.SugaredLogger, w http.ResponseWriter, span oteltrace.Span) {
addSpanEvent(span, "Started Scanning.")
numWorkers := s.config.Scanning.Workers
groupedWfps := wfpCount / s.config.Scanning.WfpGrouping
@@ -233,7 +261,7 @@ func (s APIService) scanThreaded(wfps []string, wfpCount int, flags, sbomType, s
zs.Debugf("Creating %v scanning workers...", numWorkers)
// Create workers
for i := 1; i <= numWorkers; i++ {
- go s.workerScan(fmt.Sprintf("%d_%s", i, uuid.New().String()), requests, results, flags, sbomType, sbomFile, dbName, zs)
+ go s.workerScan(fmt.Sprintf("%d_%s", i, uuid.New().String()), requests, results, sbomFile, config, zs)
}
requestCount := 0 // Count the number of actual requests sent
var wfpRequests []string
@@ -308,7 +336,7 @@ func (s APIService) validateHPSM(contents []byte, zs *zap.SugaredLogger, w http.
}
// workerScan attempts to process all incoming scanning jobs and dumps the results into the subsequent results channel.
-func (s APIService) workerScan(id string, jobs <-chan string, results chan<- string, flags, sbomType, sbomFile, dbName string, zs *zap.SugaredLogger) {
+func (s APIService) workerScan(id string, jobs <-chan string, results chan<- string, sbomFile string, config ScanningServiceConfig, zs *zap.SugaredLogger) {
if s.config.App.Trace {
zs.Debugf("Starting up scanning worker: %v", id)
}
@@ -322,7 +350,7 @@ func (s APIService) workerScan(id string, jobs <-chan string, results chan<- str
zs.Warnf("Nothing in the job request to scan. Ignoring")
results <- ""
} else {
- result, err := s.scanWfp(job, flags, sbomType, sbomFile, dbName, zs)
+ result, err := s.scanWfp(job, sbomFile, config, zs)
if s.config.App.Trace {
zs.Debugf("scan result (%v): %v, %v", id, result, err)
}
@@ -347,7 +375,7 @@ func (s APIService) workerScan(id string, jobs <-chan string, results chan<- str
}
// scanWfp run the scanoss engine scan of the supplied WFP.
-func (s APIService) scanWfp(wfp, flags, sbomType, sbomFile, dbName string, zs *zap.SugaredLogger) (string, error) {
+func (s APIService) scanWfp(wfp, sbomFile string, config ScanningServiceConfig, zs *zap.SugaredLogger) (string, error) {
if len(wfp) == 0 {
zs.Warnf("Nothing in the job request to scan. Ignoring")
return "", fmt.Errorf("no wfp supplied to scan. ignoring")
@@ -363,35 +391,53 @@ func (s APIService) scanWfp(wfp, flags, sbomType, sbomFile, dbName string, zs *z
zs.Debugf("Using temporary file: %v", tempFile.Name())
_, err = tempFile.WriteString(wfp + "\n")
if err != nil {
+ closeFile(tempFile, zs)
zs.Errorf("Failed to write WFP to temporary file: %v", err)
return "", fmt.Errorf("failed to write to temporary WFP file")
}
closeFile(tempFile, zs)
+ // Build command arguments
var args []string
if s.config.Scanning.ScanDebug {
args = append(args, "-d") // Set debug mode
}
- if len(dbName) > 0 && dbName != "" { // we want to prefer request over the local config
- args = append(args, fmt.Sprintf("-n%s", dbName))
- } else if s.config.Scanning.ScanKbName != "" { // Set scanning KB name
- args = append(args, fmt.Sprintf("-n%s", s.config.Scanning.ScanKbName))
+ // Database name
+ if len(config.dbName) > 0 {
+ args = append(args, fmt.Sprintf("-n%s", config.dbName))
}
- if s.config.Scanning.ScanFlags > 0 { // Set system flags if enabled
- args = append(args, fmt.Sprintf("-F %v", s.config.Scanning.ScanFlags))
- } else if len(flags) > 0 && flags != "0" { // Set user supplied flags if enabled
- args = append(args, fmt.Sprintf("-F %s", flags))
+ // Scanning flags
+ if config.flags > 0 {
+ args = append(args, fmt.Sprintf("-F%v", config.flags))
}
- if len(sbomFile) > 0 && len(sbomType) > 0 { // Add SBOM to scanning process
- switch sbomType {
- case "identify":
+ // SBOM configuration
+ if len(sbomFile) > 0 && len(config.sbomType) > 0 {
+ switch config.sbomType {
+ case sbomIdentify:
args = append(args, "-s")
- case "blacklist":
+ case sbomBlackList:
args = append(args, "-b")
default:
args = append(args, "-s") // Default to identify
}
args = append(args, sbomFile)
}
+ // Ranking threshold (only if ranking is enabled and allowed)
+ if config.rankingEnabled && config.rankingThreshold > 0 && s.config.Scanning.RankingAllowed {
+ args = append(args, fmt.Sprintf("-r%d", config.rankingThreshold))
+ }
+ // Minimum snippet hits
+ if config.minSnippetHits > 0 {
+ args = append(args, fmt.Sprintf("--min-snippet-hits=%d", config.minSnippetHits))
+ }
+ // Minimum snippet lines
+ if config.minSnippetLines > 0 {
+ args = append(args, fmt.Sprintf("--min-snippet-lines=%d", config.minSnippetLines))
+ }
+ // Honour file extensions (not yet implemented in scanoss engine)
+ if !config.honourFileExts {
+ args = append(args, "--ignore-file-ext")
+ }
+ // WFP file argument
args = append(args, "-w", tempFile.Name())
zs.Debugf("Executing %v %v", s.config.Scanning.ScanBinary, strings.Join(args, " "))
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.config.Scanning.ScanTimeout)*time.Second) // put a timeout on the scan execution
@@ -405,7 +451,7 @@ func (s APIService) scanWfp(wfp, flags, sbomType, sbomFile, dbName string, zs *z
}
return "", fmt.Errorf("failed to scan WFP: %v", err)
}
- return string(output), nil
+ return string(output), err
}
// TestEngine tests if the SCANOSS engine is accessible and running.
diff --git a/pkg/service/scanning_service_config.go b/pkg/service/scanning_service_config.go
new file mode 100644
index 0000000..5235b57
--- /dev/null
+++ b/pkg/service/scanning_service_config.go
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2018-2025 SCANOSS.COM
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package service
+
+import (
+ "encoding/json"
+ "fmt"
+ "strconv"
+
+ "go.uber.org/zap"
+ cfg "scanoss.com/go-api/pkg/config"
+)
+
+type ScanningServiceConfig struct {
+ flags int
+ sbomType string
+ sbomFile string
+ dbName string
+ rankingAllowed bool
+ rankingEnabled bool
+ rankingThreshold int
+ minSnippetHits int
+ minSnippetLines int
+ honourFileExts bool
+}
+
+func DefaultScanningServiceConfig(serverDefaultConfig *cfg.ServerConfig) ScanningServiceConfig {
+ return ScanningServiceConfig{
+ flags: serverDefaultConfig.Scanning.ScanFlags,
+ sbomType: "",
+ sbomFile: "",
+ dbName: serverDefaultConfig.Scanning.ScanKbName,
+ rankingAllowed: serverDefaultConfig.Scanning.RankingAllowed,
+ rankingEnabled: serverDefaultConfig.Scanning.RankingEnabled,
+ rankingThreshold: serverDefaultConfig.Scanning.RankingThreshold,
+ minSnippetHits: serverDefaultConfig.Scanning.MinSnippetHits,
+ minSnippetLines: serverDefaultConfig.Scanning.MinSnippetLines,
+ honourFileExts: serverDefaultConfig.Scanning.HonourFileExts,
+ }
+}
+
+// scanSettings represents the scanning parameters that can be configured via JSON input.
+type scanSettings 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"`
+}
+
+// applyRankingSettings updates ranking-related configuration if allowed.
+func applyRankingSettings(s *zap.SugaredLogger, config *ScanningServiceConfig, settings *scanSettings) {
+ rankingRequested := settings.RankingEnabled != nil || settings.RankingThreshold != nil
+ if rankingRequested && !config.rankingAllowed {
+ s.Warnf("Ranking settings ignored as RankingAllowed is false")
+ return
+ }
+ if settings.RankingEnabled != nil {
+ config.rankingEnabled = *settings.RankingEnabled
+ s.Debugf("Updated RankingEnabled to %v", config.rankingEnabled)
+ }
+ if settings.RankingThreshold != nil {
+ config.rankingThreshold = *settings.RankingThreshold
+ s.Debugf("Updated RankingThreshold to %d", config.rankingThreshold)
+ }
+}
+
+// applySnippetSettings updates snippet-related configuration and returns invalid setting names.
+func applySnippetSettings(s *zap.SugaredLogger, config *ScanningServiceConfig, settings *scanSettings) []string {
+ var invalidSettings []string
+ if settings.MinSnippetHits != nil {
+ if *settings.MinSnippetHits >= 0 {
+ config.minSnippetHits = *settings.MinSnippetHits
+ s.Debugf("Updated MinSnippetHits to %d", config.minSnippetHits)
+ } else {
+ invalidSettings = append(invalidSettings, fmt.Sprintf("MinSnippetHits: %d", *settings.MinSnippetHits))
+ }
+ }
+ if settings.MinSnippetLines != nil {
+ if *settings.MinSnippetLines > 0 {
+ config.minSnippetLines = *settings.MinSnippetLines
+ s.Debugf("Updated MinSnippetLines to %d", config.minSnippetLines)
+ } else {
+ invalidSettings = append(invalidSettings, fmt.Sprintf("MinSnippetLines: %d", *settings.MinSnippetLines))
+ }
+ }
+ if settings.HonourFileExts != nil {
+ config.honourFileExts = *settings.HonourFileExts
+ s.Debugf("Updated HonourFileExts to %v", config.honourFileExts)
+ }
+ return invalidSettings
+}
+
+// applyDirectParameters updates configuration from direct string parameters.
+func applyDirectParameters(s *zap.SugaredLogger, config *ScanningServiceConfig, flags, scanType, sbom, dbName string) {
+ if dbName != "" {
+ config.dbName = dbName
+ s.Debugf("Updated DbName to %s", config.dbName)
+ }
+ if flags != "" {
+ flagsInt, err := strconv.Atoi(flags)
+ if err == nil {
+ config.flags = flagsInt
+ s.Debugf("Updated Flags to %d", config.flags)
+ } else {
+ s.Errorf("Error converting flags to integer: %v", err)
+ }
+ }
+ if scanType != "" {
+ config.sbomType = scanType
+ s.Debugf("Updated SbomType to %s", config.sbomType)
+ }
+ if sbom != "" {
+ config.sbomFile = sbom
+ s.Debugf("Updated SbomFile to %s", config.sbomFile)
+ }
+}
+
+// UpdateScanningServiceConfigDTO creates an updated copy of the scanning service configuration.
+//
+// This function does NOT modify the original currentConfig. Instead, it creates a copy,
+// applies the requested updates to the copy, and returns the updated configuration.
+//
+// Parameters:
+// - s: Sugared logger for debug/error output
+// - currentConfig: Pointer to the current configuration (will NOT be modified)
+// - flags: String representation of scan flags (converted to int). Empty string = no change
+// - scanType: SBOM type to use for scanning. Empty string = no change
+// - sbom: SBOM file path. Empty string = no change
+// - dbName: Database name for scanning. Empty string = no change
+// - inputSettings: JSON bytes containing optional scan settings. Format:
+// {
+// "ranking_enabled": bool, // Enable/disable ranking (requires ranking_allowed=true)
+// "ranking_threshold": int, // Ranking threshold value (requires ranking_allowed=true)
+// "min_snippet_hits": int, // Minimum snippet hits to consider a match
+// "min_snippet_lines": int, // Minimum snippet lines to consider a match
+// "honour_file_exts": bool // Honor file extensions when filtering snippets
+// }
+//
+// Returns:
+// - A new ScanningServiceConfig with the updates applied. The original config remains unchanged.
+//
+// Note:
+// - Ranking settings (ranking_enabled, ranking_threshold) are only applied if rankingAllowed is true
+// - Invalid JSON in inputSettings will be logged and the original config will be returned
+// - Invalid flags string will be logged and that specific field will not be updated
+func UpdateScanningServiceConfigDTO(s *zap.SugaredLogger, currentConfig *ScanningServiceConfig,
+ flags, scanType, sbom, dbName string, inputSettings []byte) (ScanningServiceConfig, error) {
+ if currentConfig == nil {
+ s.Errorf("Current scanning service config is nil")
+ return ScanningServiceConfig{}, fmt.Errorf("default server scanning service config is undefined")
+ }
+ updatedConfig := *currentConfig
+ var newSettings scanSettings
+ if len(inputSettings) > 0 {
+ if err := json.Unmarshal(inputSettings, &newSettings); err != nil {
+ s.Errorf("Error unmarshalling scanning service config input: %v", err)
+ return updatedConfig, fmt.Errorf("error unmarshalling scanning service config requested by client: %v", err)
+ }
+ }
+ applyRankingSettings(s, &updatedConfig, &newSettings)
+ if invalidSettings := applySnippetSettings(s, &updatedConfig, &newSettings); len(invalidSettings) > 0 {
+ s.Errorf("Ignoring invalid values for settings: %v", invalidSettings)
+ }
+ applyDirectParameters(s, &updatedConfig, flags, scanType, sbom, dbName)
+
+ return updatedConfig, nil
+}
diff --git a/pkg/service/scanning_service_config_test.go b/pkg/service/scanning_service_config_test.go
new file mode 100644
index 0000000..6ed26a7
--- /dev/null
+++ b/pkg/service/scanning_service_config_test.go
@@ -0,0 +1,313 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2018-2025 SCANOSS.COM
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package service
+
+import (
+ "encoding/json"
+ "testing"
+
+ "go.uber.org/zap"
+ cfg "scanoss.com/go-api/pkg/config"
+)
+
+// TestDefaultScanningServiceConfig tests that default config is created correctly from server config
+func TestDefaultScanningServiceConfig(t *testing.T) {
+ serverConfig := &cfg.ServerConfig{}
+ serverConfig.Scanning.ScanFlags = 42
+ serverConfig.Scanning.ScanKbName = "test-kb"
+ serverConfig.Scanning.RankingAllowed = true
+ serverConfig.Scanning.RankingEnabled = false
+ serverConfig.Scanning.RankingThreshold = 50
+ serverConfig.Scanning.MinSnippetHits = 10
+ serverConfig.Scanning.MinSnippetLines = 5
+ serverConfig.Scanning.HonourFileExts = true
+
+ config := DefaultScanningServiceConfig(serverConfig)
+
+ if config.flags != 42 {
+ t.Errorf("Expected Flags to be 42, got %d", config.flags)
+ }
+ if config.dbName != "test-kb" {
+ t.Errorf("Expected DbName to be 'test-kb', got '%s'", config.dbName)
+ }
+ if !config.rankingAllowed {
+ t.Error("Expected RankingAllowed to be true")
+ }
+ if config.rankingEnabled {
+ t.Error("Expected RankingEnabled to be false")
+ }
+ if config.rankingThreshold != 50 {
+ t.Errorf("Expected RankingThreshold to be 50, got %d", config.rankingThreshold)
+ }
+ if config.minSnippetHits != 10 {
+ t.Errorf("Expected MinSnippetHits to be 10, got %d", config.minSnippetHits)
+ }
+ if config.minSnippetLines != 5 {
+ t.Errorf("Expected MinSnippetLines to be 5, got %d", config.minSnippetLines)
+ }
+ if !config.honourFileExts {
+ t.Error("Expected HonourFileExts to be true")
+ }
+}
+
+// TestUpdateScanningServiceConfigDTO_JSONSettings tests parsing JSON scan settings
+func TestUpdateScanningServiceConfigDTO_JSONSettings(t *testing.T) {
+ logger, _ := zap.NewDevelopment()
+ sugar := logger.Sugar()
+
+ baseConfig := ScanningServiceConfig{
+ rankingAllowed: true,
+ rankingEnabled: false,
+ rankingThreshold: 0,
+ minSnippetHits: 0,
+ minSnippetLines: 0,
+ honourFileExts: false,
+ }
+
+ // Test with multiple JSON settings
+ rankingEnabled := true
+ rankingThreshold := 75
+ minSnippetHits := 20
+ minSnippetLines := 15
+ honourFileExts := true
+
+ settings := 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"`
+ }{
+ RankingEnabled: &rankingEnabled,
+ RankingThreshold: &rankingThreshold,
+ MinSnippetHits: &minSnippetHits,
+ MinSnippetLines: &minSnippetLines,
+ HonourFileExts: &honourFileExts,
+ }
+
+ jsonBytes, err := json.Marshal(settings)
+ if err != nil {
+ t.Fatalf("Failed to marshal JSON: %v", err)
+ }
+
+ result, err := UpdateScanningServiceConfigDTO(sugar, &baseConfig, "", "", "", "", jsonBytes)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ if !result.rankingEnabled {
+ t.Error("Expected RankingEnabled to be true")
+ }
+ if result.rankingThreshold != 75 {
+ t.Errorf("Expected RankingThreshold to be 75, got %d", result.rankingThreshold)
+ }
+ if result.minSnippetHits != 20 {
+ t.Errorf("Expected MinSnippetHits to be 20, got %d", result.minSnippetHits)
+ }
+ if result.minSnippetLines != 15 {
+ t.Errorf("Expected MinSnippetLines to be 15, got %d", result.minSnippetLines)
+ }
+ if !result.honourFileExts {
+ t.Error("Expected HonourFileExts to be true")
+ }
+}
+
+// TestUpdateScanningServiceConfigDTO_RankingNotAllowed tests that ranking settings are ignored when not allowed
+func TestUpdateScanningServiceConfigDTO_RankingNotAllowed(t *testing.T) {
+ logger, _ := zap.NewDevelopment()
+ sugar := logger.Sugar()
+
+ baseConfig := ScanningServiceConfig{
+ rankingAllowed: false, // Ranking not allowed
+ rankingEnabled: false,
+ rankingThreshold: 0,
+ }
+
+ // Try to enable ranking
+ rankingEnabled := true
+ rankingThreshold := 75
+
+ settings := struct {
+ RankingEnabled *bool `json:"ranking_enabled,omitempty"`
+ RankingThreshold *int `json:"ranking_threshold,omitempty"`
+ }{
+ RankingEnabled: &rankingEnabled,
+ RankingThreshold: &rankingThreshold,
+ }
+
+ jsonBytes, err := json.Marshal(settings)
+ if err != nil {
+ t.Fatalf("Failed to marshal JSON: %v", err)
+ }
+
+ result, err := UpdateScanningServiceConfigDTO(sugar, &baseConfig, "", "", "", "", jsonBytes)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ // Should remain false because RankingAllowed is false
+ if result.rankingEnabled {
+ t.Error("Expected RankingEnabled to remain false when RankingAllowed is false")
+ }
+ if result.rankingThreshold != 0 {
+ t.Errorf("Expected RankingThreshold to remain 0 when RankingAllowed is false, got %d", result.rankingThreshold)
+ }
+}
+
+// TestUpdateScanningServiceConfigDTO_LegacyParameters tests updating legacy string parameters
+func TestUpdateScanningServiceConfigDTO_LegacyParameters(t *testing.T) {
+ logger, _ := zap.NewDevelopment()
+ sugar := logger.Sugar()
+
+ baseConfig := ScanningServiceConfig{
+ flags: 0,
+ dbName: "default-db",
+ sbomType: "",
+ sbomFile: "",
+ }
+
+ result, err := UpdateScanningServiceConfigDTO(sugar, &baseConfig,
+ "123", // flags
+ "identify", // scanType
+ "assets.json", // sbom
+ "custom-db", // dbName
+ nil)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ if result.flags != 123 {
+ t.Errorf("Expected Flags to be 123, got %d", result.flags)
+ }
+ if result.dbName != "custom-db" {
+ t.Errorf("Expected DbName to be 'custom-db', got '%s'", result.dbName)
+ }
+ if result.sbomType != "identify" {
+ t.Errorf("Expected SbomType to be 'identify', got '%s'", result.sbomType)
+ }
+ if result.sbomFile != "assets.json" {
+ t.Errorf("Expected SbomFile to be 'assets.json', got '%s'", result.sbomFile)
+ }
+}
+
+// TestUpdateScanningServiceConfigDTO_InvalidInput tests handling of invalid input
+func TestUpdateScanningServiceConfigDTO_InvalidInput(t *testing.T) {
+ logger, _ := zap.NewDevelopment()
+ sugar := logger.Sugar()
+
+ baseConfig := ScanningServiceConfig{
+ flags: 42,
+ minSnippetHits: 10,
+ }
+
+ // Test with invalid flags (should not return error, just keep original value)
+ result, err := UpdateScanningServiceConfigDTO(sugar, &baseConfig,
+ "not-a-number", "", "", "", nil)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ if result.flags != 42 {
+ t.Errorf("Expected Flags to remain 42 after invalid conversion, got %d", result.flags)
+ }
+
+ // Test with invalid JSON (should return error)
+ invalidJSON := []byte("{invalid json}")
+ _, err = UpdateScanningServiceConfigDTO(sugar, &baseConfig, "", "", "", "", invalidJSON)
+
+ if err == nil {
+ t.Error("Expected error for invalid JSON input")
+ }
+}
+
+// TestUpdateScanningServiceConfigDTO_CombinedUpdate tests updating both JSON and legacy parameters together
+func TestUpdateScanningServiceConfigDTO_CombinedUpdate(t *testing.T) {
+ logger, _ := zap.NewDevelopment()
+ sugar := logger.Sugar()
+
+ baseConfig := ScanningServiceConfig{
+ flags: 0,
+ dbName: "default-db",
+ rankingAllowed: true,
+ rankingEnabled: false,
+ rankingThreshold: 0,
+ minSnippetHits: 0,
+ }
+
+ // JSON settings
+ rankingEnabled := true
+ rankingThreshold := 80
+ minSnippetHits := 5
+
+ settings := struct {
+ RankingEnabled *bool `json:"ranking_enabled,omitempty"`
+ RankingThreshold *int `json:"ranking_threshold,omitempty"`
+ MinSnippetHits *int `json:"min_snippet_hits,omitempty"`
+ }{
+ RankingEnabled: &rankingEnabled,
+ RankingThreshold: &rankingThreshold,
+ MinSnippetHits: &minSnippetHits,
+ }
+
+ jsonBytes, err := json.Marshal(settings)
+ if err != nil {
+ t.Fatalf("Failed to marshal JSON: %v", err)
+ }
+
+ result, err := UpdateScanningServiceConfigDTO(sugar, &baseConfig,
+ "256", // flags
+ "blacklist", // scanType
+ "", // sbom
+ "prod-db", // dbName
+ jsonBytes)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ // Check JSON settings were applied
+ if !result.rankingEnabled {
+ t.Error("Expected RankingEnabled to be true")
+ }
+ if result.rankingThreshold != 80 {
+ t.Errorf("Expected RankingThreshold to be 80, got %d", result.rankingThreshold)
+ }
+ if result.minSnippetHits != 5 {
+ t.Errorf("Expected MinSnippetHits to be 5, got %d", result.minSnippetHits)
+ }
+
+ // Check legacy string parameters were applied
+ if result.flags != 256 {
+ t.Errorf("Expected Flags to be 256, got %d", result.flags)
+ }
+ if result.dbName != "prod-db" {
+ t.Errorf("Expected DbName to be 'prod-db', got '%s'", result.dbName)
+ }
+ if result.sbomType != "blacklist" {
+ t.Errorf("Expected SbomType to be 'blacklist', got '%s'", result.sbomType)
+ }
+}
+
+// TestUpdateScanningServiceConfigDTO_NilConfig tests that nil config returns an error
+func TestUpdateScanningServiceConfigDTO_NilConfig(t *testing.T) {
+ logger, _ := zap.NewDevelopment()
+ sugar := logger.Sugar()
+
+ _, err := UpdateScanningServiceConfigDTO(sugar, nil, "", "", "", "", nil)
+
+ if err == nil {
+ t.Error("Expected error when currentConfig is nil")
+ }
+}
diff --git a/pkg/service/scanning_service_test.go b/pkg/service/scanning_service_test.go
index 9bb5a3d..83e813c 100644
--- a/pkg/service/scanning_service_test.go
+++ b/pkg/service/scanning_service_test.go
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
- * Copyright (C) 2018-2023 SCANOSS.COM
+ * Copyright (C) 2018-2025 SCANOSS.COM
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/test-support/scanoss.sh b/test-support/scanoss.sh
index b79a1e3..1bffd84 100755
--- a/test-support/scanoss.sh
+++ b/test-support/scanoss.sh
@@ -27,6 +27,11 @@ fi
if [ "$1" == "-k" ] || [ "$2" == "-k" ] || [ "$3" == "-k" ] ; then
for i in "$@"; do :; done
md5=$i
+ # Validate MD5 format (32 hexadecimal characters)
+ if [[ ! "$md5" =~ ^[a-fA-F0-9]{32}$ ]]; then
+ echo "Error: Invalid MD5 hash format: $md5"
+ exit 1
+ fi
echo "file contents: $md5"
echo "line 2"
echo "line 3"
@@ -51,26 +56,30 @@ if [ "$1" == "-l" ] || [ "$2" == "-l" ] || [ "$3" == "-l" ] ; then
exit 0
fi
-# Simulate invalid kb name
+# Simulate kb name validation
for arg in "$@"; do
- if [[ "$arg" == "-n"* ]]; then
- # Extract everything after "-n"
- scf=${arg#-n}
- # Only show error if the value is NOT "oss"
- if [[ "$scf" != "oss" ]]; then
- echo "{Error: file and url tables must be present in $scf KB in order to proceed with the scan"
- exit 1
- fi
+ if [[ "$arg" == "-n" ]]; then
+ # -n followed by space (separate argument) is invalid - KB name should be attached
+ echo "Error: -n flag requires a KB name (use -n)" >&2
+ exit 1
+ fi
+ # Check for invalid KB name (test_kb is used in tests to simulate invalid KB)
+ if [[ "$arg" == "-ntest_kb" ]]; then
+ echo "Error: KB 'test_kb' not found" >&2
+ exit 1
fi
done
# Simulate return a scan result
-if [ "$1" == "-w" ] || [ "$2" == "-w" ] || [ "$3" == "-w" ] || [ "$4" == "-w" ] || [ "$5" == "-w" ] || [ "$6" == "-w" ] || [ "$7" == "-w" ] || [ "$8" == "-w" ]; then
- for i in "$@"; do :; done
- scf=$i
- echo " {\"$scf\":[{\"id\": \"none\", \"server\": { \"kb_version\": {\"daily\": \"23.08.09\", \"monthly\": \"23.07\"}, \"version\": \"5.2.7\"}}]} "
- exit 0
-fi
+# Check if -w is present anywhere in the arguments
+for arg in "$@"; do
+ if [[ "$arg" == "-w" ]]; then
+ for i in "$@"; do :; done
+ scf=$i
+ echo " {\"$scf\":[{\"id\": \"none\", \"server\": { \"kb_version\": {\"daily\": \"23.08.09\", \"monthly\": \"23.07\"}, \"version\": \"5.2.7\"}}]} "
+ exit 0
+ fi
+done
# Unknown command option, respond with error
echo "Unknown command option: $*"
diff --git a/tests/charset_detection_test.go b/tests/charset_detection_test.go
index 405f24f..cbebcc0 100644
--- a/tests/charset_detection_test.go
+++ b/tests/charset_detection_test.go
@@ -18,10 +18,11 @@ package tests
import (
"fmt"
- "github.com/stretchr/testify/suite"
"io"
"net/http"
"testing"
+
+ "github.com/stretchr/testify/suite"
)
type E2ECharsetDetectionSuite struct {
@@ -39,6 +40,9 @@ func (s *E2ECharsetDetectionSuite) TestFileContentsWithCharsetHeader() {
if err != nil {
s.Failf("an error was not expected when sending request.", "error: %v", err)
}
+ if resp.StatusCode == http.StatusForbidden {
+ s.T().Skip("skipping test: file_contents endpoint returned 403 Forbidden")
+ }
s.Equal(http.StatusOK, resp.StatusCode)
// Check Content-Type header includes charset.
@@ -75,6 +79,9 @@ func (s *E2ECharsetDetectionSuite) TestFileContentsWithInvalidMD5() {
s.Failf("an error was not expected when sending request.", "error: %v", err)
}
// Should return an error status since the MD5 is invalid.
+ if resp.StatusCode == http.StatusForbidden {
+ s.T().Skip("skipping test: file_contents endpoint returned 403 Forbidden")
+ }
s.Equal(http.StatusInternalServerError, resp.StatusCode)
}
@@ -85,6 +92,9 @@ func (s *E2ECharsetDetectionSuite) TestFileContentsWithMissingMD5() {
if err != nil {
s.Failf("an error was not expected when sending request.", "error: %v", err)
}
+ if resp.StatusCode == http.StatusForbidden {
+ s.T().Skip("skipping test: file_contents endpoint returned 403 Forbidden")
+ }
// Should return not found since the path is incomplete.
s.Equal(http.StatusNotFound, resp.StatusCode)
-}
\ No newline at end of file
+}
diff --git a/tests/file_contents_test.go b/tests/file_contents_test.go
index 58fd054..c059619 100644
--- a/tests/file_contents_test.go
+++ b/tests/file_contents_test.go
@@ -38,6 +38,9 @@ func (s *E2EContentsSuite) TestHappyFileContents() {
if err != nil {
s.Failf("an error was not expected when sending request.", "error: %v", err)
}
+ if resp.StatusCode == http.StatusForbidden {
+ s.T().Skip("skipping test: file_contents endpoint returned 403 Forbidden")
+ }
s.Equal(http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
if err != nil {
diff --git a/tests/scanning_test.go b/tests/scanning_test.go
index c921c18..91095ec 100644
--- a/tests/scanning_test.go
+++ b/tests/scanning_test.go
@@ -48,7 +48,7 @@ func (s *E2EScanningSuite) TestScanning() {
filename: "../pkg/service/tests/fingers.wfp",
shortName: "fingers.wfp",
extraFields: map[string]string{"db_name": "test_kb"},
- want: http.StatusInternalServerError,
+ want: http.StatusInternalServerError, // Engine returns error for invalid KB names
},
{
name: "Test Empty WFP",
@@ -90,14 +90,14 @@ func (s *E2EScanningSuite) TestScanning() {
name: "Test Flags - identify",
filename: "../pkg/service/tests/fingers.wfp",
shortName: "fingers.wfp",
- extraFields: map[string]string{"flags": "16", "type": "identify", "assets": "pkg:github/org/repo"},
+ extraFields: map[string]string{"flags": "16", "type": "identify", "assets": `{"components":[{"purl":"pkg:github/scanoss/scanoss.py"}]}`},
want: http.StatusOK,
},
{
name: "Test Flags - blacklist",
filename: "../pkg/service/tests/fingers.wfp",
shortName: "fingers.wfp",
- extraFields: map[string]string{"flags": "16", "type": "blacklist", "assets": "pkg:github/org/repo"},
+ extraFields: map[string]string{"flags": "16", "type": "blacklist", "assets": `{"components":[{"purl":"pkg:github/scanoss/scanoss.py"}]}`},
want: http.StatusOK,
},
}
@@ -132,3 +132,91 @@ func (s *E2EScanningSuite) TestScanning() {
})
}
}
+
+func (s *E2EScanningSuite) TestScanSettingsHeader() {
+ c := http.Client{}
+ tests := []struct {
+ name string
+ filename string
+ shortName string
+ scanSettingsB64 string
+ extraFields map[string]string
+ want int
+ description string
+ }{
+ {
+ name: "Test Valid ScanSettings - Multiple Settings",
+ filename: "../pkg/service/tests/fingers.wfp",
+ shortName: "fingers.wfp",
+ extraFields: map[string]string{},
+ want: http.StatusOK,
+ description: "Should successfully process valid scan settings with multiple parameters",
+ // Base64 decoded JSON:
+ // {
+ // "ranking_enabled": true,
+ // "ranking_threshold": 85,
+ // "min_snippet_hits": 3,
+ // "min_snippet_lines": 8,
+ // "honour_file_exts": false
+ // }
+ scanSettingsB64: "eyJyYW5raW5nX2VuYWJsZWQiOnRydWUsInJhbmtpbmdfdGhyZXNob2xkIjo4NSwibWluX3NuaXBwZXRfaGl0cyI6MywibWluX3NuaXBwZXRfbGluZXMiOjgsImhvbm91cl9maWxlX2V4dHMiOmZhbHNlfQ==",
+ },
+ {
+ name: "Test Invalid ScanSettings - Invalid Base64",
+ filename: "../pkg/service/tests/fingers.wfp",
+ shortName: "fingers.wfp",
+ scanSettingsB64: "invalid-base64!!!", // Invalid base64 string - should return error
+ extraFields: map[string]string{},
+ want: http.StatusBadRequest, // Invalid scan settings should return error
+ description: "Should return error for invalid base64 scan settings",
+ },
+ {
+ name: "Test ScanSettings with Legacy Flags",
+ filename: "../pkg/service/tests/fingers.wfp",
+ shortName: "fingers.wfp",
+ extraFields: map[string]string{"flags": "16"},
+ want: http.StatusOK,
+ description: "Should successfully combine scan settings with legacy flags parameter",
+ // Base64 decoded JSON:
+ // {
+ // "min_snippet_hits": 5,
+ // "min_snippet_lines": 10
+ // }
+ scanSettingsB64: "eyJtaW5fc25pcHBldF9oaXRzIjo1LCJtaW5fc25pcHBldF9saW5lcyI6MTB9",
+ },
+ }
+
+ for _, test := range tests {
+ s.Run(test.name, func() {
+ b, w, err := createMultipartFormData("file", test.filename, test.shortName, test.extraFields)
+ if err != nil {
+ s.Failf("an error was not creating multipart form data.", "error: %v", err)
+ }
+ req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%v/scan/direct", hostPort), &b)
+ if err != nil {
+ s.Failf("an error was not creating request.", "error: %v", err)
+ }
+ req.Header.Set("Content-Type", w.FormDataContentType())
+
+ // Set the Scanoss-Settings header if provided
+ if len(test.scanSettingsB64) > 0 {
+ req.Header.Set("Scanoss-Settings", test.scanSettingsB64)
+ }
+
+ resp, err := c.Do(req)
+ if err != nil {
+ s.Failf("an error was not expected when sending request.", "error: %v", err)
+ }
+
+ s.Equal(test.want, resp.StatusCode, test.description)
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ s.Failf("an error was not expected when reading response body.", "error: %v", err)
+ }
+ fmt.Println("Test: ", test.name)
+ fmt.Println("Status: ", resp.StatusCode)
+ fmt.Println("Type: ", resp.Header.Get("Content-Type"))
+ fmt.Println("Body: ", string(body))
+ })
+ }
+}