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)) + }) + } +}