diff --git a/sonarqube/.gitattributes b/sonarqube/.gitattributes new file mode 100644 index 00000000..3a454933 --- /dev/null +++ b/sonarqube/.gitattributes @@ -0,0 +1,4 @@ +/dagger.gen.go linguist-generated +/internal/dagger/** linguist-generated +/internal/querybuilder/** linguist-generated +/internal/telemetry/** linguist-generated diff --git a/sonarqube/.gitignore b/sonarqube/.gitignore new file mode 100644 index 00000000..773338bf --- /dev/null +++ b/sonarqube/.gitignore @@ -0,0 +1,5 @@ +/dagger.gen.go +/internal/dagger +/internal/querybuilder +/internal/telemetry +/.env diff --git a/sonarqube/dagger.json b/sonarqube/dagger.json new file mode 100644 index 00000000..38381c5d --- /dev/null +++ b/sonarqube/dagger.json @@ -0,0 +1,7 @@ +{ + "name": "sonarqube", + "engineVersion": "v0.21.6", + "sdk": { + "source": "go" + } +} diff --git a/sonarqube/go.mod b/sonarqube/go.mod new file mode 100644 index 00000000..14d697e3 --- /dev/null +++ b/sonarqube/go.mod @@ -0,0 +1,57 @@ +module dagger/sonarqube + +go 1.26.3 + +require ( + github.com/Khan/genqlient v0.8.1 + github.com/dagger/otel-go v1.43.0 + github.com/vektah/gqlparser/v2 v2.5.33 + go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 +) + +require dagger.io/dagger v0.21.5-0.20260611000309-336f7b79a6df + +require ( + github.com/99designs/gqlgen v0.17.90 // indirect + github.com/adrg/xdg v0.5.3 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/sosodev/duration v1.4.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 // indirect + go.opentelemetry.io/otel/log v0.17.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/sdk/log v0.17.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) + +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 + +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 + +replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.16.0 + +replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.16.0 diff --git a/sonarqube/go.sum b/sonarqube/go.sum new file mode 100644 index 00000000..9b2aaa6a --- /dev/null +++ b/sonarqube/go.sum @@ -0,0 +1,103 @@ +dagger.io/dagger v0.21.5-0.20260611000309-336f7b79a6df h1:gSiY/B1ABiQBX7Igt8kRZA1dlKKW7zdutFEsppVw52s= +dagger.io/dagger v0.21.5-0.20260611000309-336f7b79a6df/go.mod h1:8hztpM9rKNjSmOa2nzd8ot6aw1RZFDWCawMfc5V9tXA= +github.com/99designs/gqlgen v0.17.90 h1:wSv6blm/PoplU6QoNw83EcQpNtC0HX3/+44vITJOzpk= +github.com/99designs/gqlgen v0.17.90/go.mod h1:GqYrEwYsqCG8VaOsq2kJUCUKwAE1T+u2i+Nj7NtXiVI= +github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= +github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= +github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= +github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +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/dagger/otel-go v1.43.0 h1:AYCnAamWmxtSxigWPTgC+8EWqiWPcDZEegh8y05gdJ8= +github.com/dagger/otel-go v1.43.0/go.mod h1:83CTuXi70zcx1kaym5buqmb7RNzg1E9dEiQSFyLbLdU= +github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 h1:g6vfdGRyz6fAjfHz5FyYPZgHy8qcQ31fHrBl1iCOzxw= +github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59/go.mod h1:jsdUJeYzcbyK1j/EqMGPrQgNYxl/Zfg06vvM9C/xXxs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= +github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vektah/gqlparser/v2 v2.5.33 h1:lRp8aIeNUNbimf/axZd7ETg24q06hBtPaas+TcvI/7E= +github.com/vektah/gqlparser/v2 v2.5.33/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +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/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0 h1:VO3BL6OZXRQ1yQc8W6EVfJzINeJ35BkiHx4MYfoQf44= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.41.0/go.mod h1:qRDnJ2nv3CQXMK2HUd9K9VtvedsPAce3S+/4LZHjX/s= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0 h1:MMrOAN8H1FrvDyq9UJ4lu5/+ss49Qgfgb7Zpm0m8ABo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.41.0/go.mod h1:Na+2NNASJtF+uT4NxDe0G+NQb+bUgdPDfwxY/6JmS/c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= +go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= +go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= +go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +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-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= +google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sonarqube/main.go b/sonarqube/main.go new file mode 100644 index 00000000..4ad1049d --- /dev/null +++ b/sonarqube/main.go @@ -0,0 +1,289 @@ +// Sonarqube module for local development use/scanning ONLY. +// This module will start a sonar server as a service, +// run a scan with sonar-scanner CLI with a given source directory, +// and return a json report of any issues found. + +package main + +import ( + "context" + "crypto/rand" + "dagger/sonarqube/internal/dagger" + "fmt" + "strings" + "time" +) + +type Sonarqube struct{} + +const changePasswordScript = ` +http_code=$(curl -s --retry 5 --retry-delay 2 --noproxy "*" -X POST -u admin:admin \ + --data-urlencode "login=admin" \ + --data-urlencode "previousPassword=admin" \ + --data-urlencode "password=$SONAR_ADMIN_TOKEN" \ + -o /tmp/pw_res \ + -w "%{http_code}" \ + http://sonar-server:9000/api/users/change_password) + +if [ "$http_code" -ne 204 ]; then + echo "HTTP Error $http_code: $(cat /tmp/pw_res)" + exit 1 +fi +` + +const createProjectScript = ` +http_code=$(curl -s --retry 5 --retry-delay 2 --noproxy "*" -X POST -u admin:$SONAR_ADMIN_TOKEN \ + -d "project=proj1" \ + -d "name=proj1" \ + -o /tmp/proj_res \ + -w "%{http_code}" \ + http://sonar-server:9000/api/projects/create) + +if [ "$http_code" -ne 200 ]; then + echo "HTTP Error $http_code: $(cat /tmp/proj_res)" + exit 1 +fi +` + +const generateTokenScript = ` +http_code=$(curl -s --retry 5 --retry-delay 2 --noproxy "*" -X POST -u admin:$SONAR_ADMIN_TOKEN \ + -d "name=dagger-token" \ + -d "type=USER_TOKEN" \ + -d "projectKey=proj1" \ + -o /tmp/token_res \ + -w "%{http_code}" \ +http://sonar-server:9000/api/user_tokens/generate) + +if [ "$http_code" -ne 200 ]; then + echo "HTTP Error $http_code: $(cat /tmp/token_res)" + exit 1 +fi + +# extract token +cat /tmp/token_res | jq -r '.token' +` + +const pollScript = ` + echo "Polling SonarQube until analysis processing completes..." >&2 + while true; do + res=$(curl -s --noproxy "*" -u "$SONAR_TOKEN:" "http://sonar-server:9000/api/ce/component?component=proj1") + queue_length=$(echo "$res" | jq '.queue | length') + current_status=$(echo "$res" | jq -r '.current.status // "NONE"') + + echo "Current status: $current_status | Tasks in queue: $queue_length" >&2 + if [ "$queue_length" -eq 0 ] && [ "$current_status" = "SUCCESS" ]; then + echo "Analysis complete and successful!" >&2 + break + fi + sleep 2 + done + sleep 2 # Cooldown for index stabilization + ` + +// start up sonar-server as a service +func (m *Sonarqube) Service() *dagger.Service { + + return dag.Container(). + From("sonarqube:community"). + WithEnvVariable("SONAR_ES_BOOTSTRAP_CHECKS_DISABLE", "true"). + // Define a system passcode that skips user authentication for health metrics + WithEnvVariable("SONAR_WEB_SYSTEMPASSCODE", "dagger-health-token"). + WithExposedPort(9000). + WithDockerHealthcheck( + []string{ + "sh", + "-c", + `curl -sf --noproxy "*" -H "X-Sonar-Passcode: dagger-health-token" http://localhost:9000/api/system/health | grep -q '"health":"GREEN"'`, + }, + dagger.ContainerWithDockerHealthcheckOpts{ + Interval: "5s", + Timeout: "3s", + Retries: 30, + }, + ).AsService() +} + +// scan a source directory with sonar-scanner and get a report from sonar-server +func (m *Sonarqube) Scan(ctx context.Context, + // +defaultPath="/" + src *dagger.Directory, + // comma separated list of impact severities to use when generating report + // +optional + // +default="MEDIUM,HIGH" + impactSeverities string) (*dagger.File, error) { + //start sonar-server + sonarSvc, err := m.Service().Start(ctx) + + if err != nil { + return nil, fmt.Errorf("failed to start sonar service: %w", err) + } + + // defer sonarSvc.Stop(ctx) + + // change admin pw on first use + adminToken := m.generateRandomTokenAsSecret() + + //setup admin pw and project in sonar + if err := m.serverSetup(ctx, sonarSvc, adminToken); err != nil { + return nil, err + } + + //generate sonar token + sonarToken, err := m.generateSonarToken(ctx, sonarSvc, adminToken) + + // run sonar scan + _, err = dag.Container(). + From("sonarsource/sonar-scanner-cli:latest"). + WithServiceBinding("sonar-server", sonarSvc). + WithDirectory("/src", src, dagger.ContainerWithDirectoryOpts{Owner: "scanner-cli"}). + WithWorkdir("/src"). + WithSecretVariable("SONAR_TOKEN", sonarToken). + WithEnvVariable("SONAR_HOST_URL", "http://sonar-server:9000"). + WithEnvVariable("CACHEBUSTER", time.Now().String()). + WithExec([]string{ + "sonar-scanner", + "-Dsonar.projectName=proj1", + "-Dsonar.projectKey=proj1", + }).Sync(ctx) + + if err != nil { + return nil, fmt.Errorf("failed to run sonarqube scan: %w", err) + } + + // get json report of issues + report := m.getReport(sonarSvc, sonarToken, impactSeverities) + + return report, nil +} + +// generate random sonar admin pw and return as a secret +func (m *Sonarqube) generateRandomTokenAsSecret() *dagger.Secret { + const ( + lower = "abcdefghijklmnopqrstuvwxyz" + upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + numbers = "0123456789" + special = "!@#%" + all = lower + upper + numbers + special + ) + + b := make([]byte, 16) + + // 1 guaranteed from each required set + b[0] = lower[m.randomInt(len(lower))] + b[1] = upper[m.randomInt(len(upper))] + b[2] = numbers[m.randomInt(len(numbers))] + b[3] = special[m.randomInt(len(special))] + + // fill remaining with full charset + for i := 4; i < 16; i++ { + b[i] = all[m.randomInt(len(all))] + } + + // shuffle so pattern isn't predictable + m.shuffle(b) + + token := string(b) + + return dag.SetSecret("SONAR_ADMIN_TOKEN", token) +} + +// helper for token gen +func (m *Sonarqube) randomInt(max int) int { + b := make([]byte, 1) + _, _ = rand.Read(b) + return int(b[0]) % max +} + +// helper for token gen +func (m *Sonarqube) shuffle(b []byte) { + for i := len(b) - 1; i > 0; i-- { + j := m.randomInt(i + 1) + b[i], b[j] = b[j], b[i] + } +} + +// base curl container for sonar api queries +func (m *Sonarqube) curlCtr(svc *dagger.Service) *dagger.Container { + return dag.Container(). + From("alpine:latest"). + WithExec([]string{"apk", "add", "curl", "jq"}). + WithServiceBinding("sonar-server", svc) +} + +// create sonar project token to run a scan with +func (m *Sonarqube) generateSonarToken(ctx context.Context, svc *dagger.Service, adminToken *dagger.Secret) (*dagger.Secret, error) { + + token, err := m.curlCtr(svc). + WithEnvVariable("CACHEBUSTER", time.Now().String()). + WithSecretVariable("SONAR_ADMIN_TOKEN", adminToken). + WithExec([]string{"sh", "-c", generateTokenScript}).Stdout(ctx) + + if err != nil { + return nil, fmt.Errorf("failed to automatically create sonar token: %w", err) + } + + return dag.SetSecret("SONAR_TOKEN", strings.TrimSpace(token)), nil + +} + +// get generated report in sonar +func (m *Sonarqube) getReport(svc *dagger.Service, token *dagger.Secret, impactSeverities string) *dagger.File { + + fetchScript := ` + PAGE_SIZE=100 + BASE_URL="http://sonar-server:9000/api/issues/search?components=proj1&impactSeverities=$IMPACT_SEVERITIES&ps=$PAGE_SIZE" + + echo "Fetching initial page..." >&2 + first_page=$(curl -s --retry 5 --noproxy "*" -u "$SONAR_TOKEN:" "${BASE_URL}&p=1") + total_issues=$(echo "$first_page" | jq '.paging.total // 0') + + # Write the first page as a compressed single line (-c flag minimizes it to 1 line) + echo "$first_page" | jq -c '.' > /tmp/report.jsonl + + total_pages=$(( (total_issues + PAGE_SIZE - 1) / PAGE_SIZE )) + + if [ "$total_pages" -gt 1 ]; then + for p in $(seq 2 $total_pages); do + echo "Fetching page $p of $total_pages..." >&2 + next_page=$(curl -s --retry 5 --noproxy "*" -u "$SONAR_TOKEN:" "${BASE_URL}&p=$p") + + # Append subsequent raw pages as single lines + echo "$next_page" | jq -c '.' >> /tmp/report.jsonl + done + fi + + # Output the combined multi-document file + cat /tmp/report.jsonl + ` + + return m.curlCtr(svc). + WithSecretVariable("SONAR_TOKEN", token). + WithEnvVariable("IMPACT_SEVERITIES", impactSeverities). + WithEnvVariable("CACHEBUSTER", time.Now().String()). + // Wait for report generation to finish + WithExec([]string{"sh", "-c", pollScript}). + // Fetch report + WithExec([]string{"sh", "-c", fetchScript}, dagger.ContainerWithExecOpts{ + RedirectStdout: "sonar-report.json", + }). + File("sonar-report.json") + +} + +// create initial admin password and create new project with project name +func (m *Sonarqube) serverSetup(ctx context.Context, svc *dagger.Service, adminToken *dagger.Secret) error { + + curlCtr := m.curlCtr(svc).WithSecretVariable("SONAR_ADMIN_TOKEN", adminToken) + // change admin PW and create project in sonar-server + Out, err := curlCtr. + WithEnvVariable("CACHEBUSTER", time.Now().String()). + WithExec([]string{"sh", "-c", changePasswordScript}, dagger.ContainerWithExecOpts{}). + WithExec([]string{"sh", "-c", createProjectScript}). + Stdout(ctx) + + if err != nil { + return fmt.Errorf("sonarqube server setup failed: %w\nDetails: %s", err, Out) + } + + return nil +}