diff --git a/.circleci/config.yml b/.circleci/config.yml index ba4ada3ca9..4b5571e46f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,7 +30,7 @@ parameters: go_version: type: string # https://go.dev/doc/devel/release - default: '1.18.2' + default: '1.19.4' mitmproxy_version: type: string # https://go.dev/doc/devel/release @@ -39,7 +39,7 @@ parameters: executors: alpine: docker: - - image: alpine:3.16.2 + - image: alpine:3.17 docker-node: parameters: node_version: @@ -52,11 +52,11 @@ executors: linux: machine: # https://circleci.com/developer/machine/image/ubuntu-2004 - image: ubuntu-2004:202201-01 + image: ubuntu-2004:2022.04.1 linux-arm64: machine: # https://circleci.com/docs/2.0/arm-resources/ - image: ubuntu-2004:202101-01 + image: ubuntu-2004:2022.04.1 resource_class: arm.medium macos: macos: @@ -475,11 +475,13 @@ jobs: - attach_workspace: at: . - install_sdks_linux + - go/install: + version: << pipeline.parameters.go_version >> - run: - name: Installing golang + name: Installing test dependencies command: | sudo apt-get update - sudo apt-get install golang gradle python3 python3-pip elixir composer + sudo apt-get install gradle python3 python3-pip elixir composer - setup_npm: npm_global_sudo: << parameters.npm_global_sudo >> node_version: << parameters.node_version >> diff --git a/cliv2/cmd/cliv2/main.go b/cliv2/cmd/cliv2/main.go index f00cd8dce6..eb33316b05 100644 --- a/cliv2/cmd/cliv2/main.go +++ b/cliv2/cmd/cliv2/main.go @@ -88,11 +88,18 @@ func sendAnalytics(analytics analytics.Analytics, debugLogger *log.Logger) { debugLogger.Println("Sending Analytics") res, err := analytics.Send() - errorCodeReceived := res != nil && 200 <= res.StatusCode && res.StatusCode < 300 - if err == nil && !errorCodeReceived { + successfullySend := res != nil && 200 <= res.StatusCode && res.StatusCode < 300 + if err == nil && successfullySend { debugLogger.Println("Analytics sucessfully send") } else { - debugLogger.Println("Failed to send Analytics:", err) + var details string + if res != nil { + details = res.Status + } else if err != nil { + details = err.Error() + } + + debugLogger.Println("Failed to send Analytics:", details) } } @@ -233,6 +240,16 @@ func MainWithErrorCode() int { networkAccess := engine.GetNetworkAccess() networkAccess.AddHeaderField("x-snyk-cli-version", cliv2.GetFullVersion()) + extraCaCertFile := config.GetString(constants.SNYK_CA_CERTIFICATE_LOCATION_ENV) + if len(extraCaCertFile) > 0 { + err = networkAccess.AddRootCAs(extraCaCertFile) + if err != nil { + debugLogger.Printf("Failed to AddRootCAs from '%s' (%v)\n", extraCaCertFile, err) + } else { + debugLogger.Println("Using additional CAs from file:", extraCaCertFile) + } + } + // init Analytics cliAnalytics := engine.GetAnalytics() cliAnalytics.SetVersion(cliv2.GetFullVersion()) diff --git a/cliv2/cmd/make-cert/main.go b/cliv2/cmd/make-cert/main.go index 8c72f8baf5..fa42affabe 100644 --- a/cliv2/cmd/make-cert/main.go +++ b/cliv2/cmd/make-cert/main.go @@ -7,8 +7,8 @@ import ( "path" "strings" - "github.com/snyk/cli/cliv2/internal/certs" "github.com/snyk/cli/cliv2/internal/utils" + "github.com/snyk/go-application-framework/pkg/networking/certs" ) func main() { diff --git a/cliv2/go.mod b/cliv2/go.mod index 0cd740a0e7..bca9be2876 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 github.com/snyk/cli-extension-sbom v0.0.0-20221212093410-6b474ed1a42a - github.com/snyk/go-application-framework v0.0.0-20221213122015-81ad8dd6311d + github.com/snyk/go-application-framework v0.0.0-20221215182111-b2d10cf1e146 github.com/snyk/go-httpauth v0.0.0-20220915135832-0edf62cf8cdd github.com/spf13/cobra v1.6.0 github.com/spf13/pflag v1.0.5 diff --git a/cliv2/go.sum b/cliv2/go.sum index 6dca9c5e15..a8e7fb41e8 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -184,8 +184,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/snyk/cli-extension-sbom v0.0.0-20221212093410-6b474ed1a42a h1:kImXWA4kbwaREeC+kaJ8H0aOukWzpK8K/UzAsExj6MU= github.com/snyk/cli-extension-sbom v0.0.0-20221212093410-6b474ed1a42a/go.mod h1:ohrrgC94Gx82/cgSiac02JQrsMjFtggvhAvXGuGjDGU= -github.com/snyk/go-application-framework v0.0.0-20221213122015-81ad8dd6311d h1:5//WGQrFXri33xGuLgVEHOsBD0aU2ZHU8JFEGJBBc68= -github.com/snyk/go-application-framework v0.0.0-20221213122015-81ad8dd6311d/go.mod h1:5hLGqObbxLWnZkhn3Xc5PblESjQOfjN509ucQ4dtqz8= +github.com/snyk/go-application-framework v0.0.0-20221215182111-b2d10cf1e146 h1:V5kc8tSGVhyiPNuEXkZ9CVmwWiYlMmaQGpjRbORuqlU= +github.com/snyk/go-application-framework v0.0.0-20221215182111-b2d10cf1e146/go.mod h1:5hLGqObbxLWnZkhn3Xc5PblESjQOfjN509ucQ4dtqz8= github.com/snyk/go-httpauth v0.0.0-20220915135832-0edf62cf8cdd h1:zjDhcQ642rIVI8aIjfG5uVcw+OGotQtX2l9VHe7IqCQ= github.com/snyk/go-httpauth v0.0.0-20220915135832-0edf62cf8cdd/go.mod h1:v6t6wKizOcHXT3p4qKn6Bda7yNIjCQ54Xyl31NjgXkY= github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= diff --git a/cliv2/internal/certs/certs.go b/cliv2/internal/certs/certs.go deleted file mode 100644 index a57ff1e000..0000000000 --- a/cliv2/internal/certs/certs.go +++ /dev/null @@ -1,69 +0,0 @@ -package certs - -import ( - "bytes" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "log" - "math/big" - "time" -) - -func MakeSelfSignedCert(certName string, dnsNames []string, debugLogger *log.Logger) (certPEMBlock []byte, keyPEMBlock []byte, err error) { - // create a key - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, err - } - - // create a self-signed cert using the key - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: certName, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 365), - - KeyUsage: x509.KeyUsageDigitalSignature | - x509.KeyUsageKeyEncipherment | - x509.KeyUsageKeyAgreement | - x509.KeyUsageCertSign, // needed for sure - - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - IsCA: true, - } - - for _, dnsName := range dnsNames { - template.DNSNames = append(template.DNSNames, dnsName) - debugLogger.Println("MakeSelfSignedCert added ", dnsName) - } - - certDERBytes, err_CreateCertificate := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) - if err_CreateCertificate != nil { - return nil, nil, err - } - - certPEMBytesBuffer := &bytes.Buffer{} - if err := pem.Encode(certPEMBytesBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: certDERBytes}); err != nil { - fmt.Println(err) - return nil, nil, err - } - - // make the key pem - keyDERBytes := x509.MarshalPKCS1PrivateKey(privateKey) - keyPEMBytesBuffer := &bytes.Buffer{} - if err := pem.Encode(keyPEMBytesBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDERBytes}); err != nil { - return nil, nil, err - } - - certPEMBlockBytes := certPEMBytesBuffer.Bytes() - keyPEMBlockBytes := keyPEMBytesBuffer.Bytes() - - return certPEMBlockBytes, keyPEMBlockBytes, nil -} diff --git a/cliv2/internal/proxy/proxy.go b/cliv2/internal/proxy/proxy.go index d5192d7c72..c0da976e98 100644 --- a/cliv2/internal/proxy/proxy.go +++ b/cliv2/internal/proxy/proxy.go @@ -13,8 +13,9 @@ import ( "github.com/google/uuid" - "github.com/snyk/cli/cliv2/internal/certs" + "github.com/snyk/cli/cliv2/internal/constants" "github.com/snyk/cli/cliv2/internal/utils" + "github.com/snyk/go-application-framework/pkg/networking/certs" "github.com/snyk/go-httpauth/pkg/httpauth" "github.com/elazarl/goproxy" @@ -71,7 +72,33 @@ func NewWrapperProxy(insecureSkipVerify bool, cacheDirectory string, cliVersion defer certFile.Close() p.CertificateLocation = certFile.Name() // gives full path, not just the name - p.DebugLogger.Println("p.CertificateLocation:", p.CertificateLocation) + + rootCAs, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + + // append any given extra CA certificate to the internal PEM data before storing it to file + // this merges user provided CA certificates with the internal one + if extraCaCertFile, ok := os.LookupEnv(constants.SNYK_CA_CERTIFICATE_LOCATION_ENV); ok { + extraCertificateBytes, extraCertificateList, extraCertificateError := certs.GetExtraCaCert(extraCaCertFile) + if extraCertificateError == nil { + // add to pem data + certPEMBlock = append(certPEMBlock, '\n') + certPEMBlock = append(certPEMBlock, extraCertificateBytes...) + + // add to cert pool + for _, currentCert := range extraCertificateList { + if currentCert != nil { + rootCAs.AddCert(currentCert) + } + } + + p.DebugLogger.Println("Using additional CAs from file: ", extraCaCertFile) + } + } + + p.DebugLogger.Println("Temporary CertificateLocation:", p.CertificateLocation) certPEMString := string(certPEMBlock) err = utils.WriteToFile(p.CertificateLocation, certPEMString) @@ -88,6 +115,7 @@ func NewWrapperProxy(insecureSkipVerify bool, cacheDirectory string, cliVersion p.transport = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: insecureSkipVerify, // goproxy defaults to true + RootCAs: rootCAs, }, } diff --git a/cliv2/internal/proxy/proxy_test.go b/cliv2/internal/proxy/proxy_test.go index cfc1c24765..2a0232a4d0 100644 --- a/cliv2/internal/proxy/proxy_test.go +++ b/cliv2/internal/proxy/proxy_test.go @@ -13,7 +13,9 @@ import ( "os" "testing" + "github.com/snyk/cli/cliv2/internal/constants" "github.com/snyk/cli/cliv2/internal/proxy" + "github.com/snyk/go-application-framework/pkg/networking/certs" "github.com/snyk/go-httpauth/pkg/httpauth" "github.com/stretchr/testify/assert" @@ -218,3 +220,25 @@ func Test_SetUpstreamProxy(t *testing.T) { } } } + +func Test_appendExtraCaCert(t *testing.T) { + certPem, _, _ := certs.MakeSelfSignedCert("mycert", []string{"dns"}, debugLogger) + file, _ := os.CreateTemp("", "") + file.Write(certPem) + + os.Setenv(constants.SNYK_CA_CERTIFICATE_LOCATION_ENV, file.Name()) + + wp, err := proxy.NewWrapperProxy(false, "", "", debugLogger) + assert.Nil(t, err) + + certsPem, err := os.ReadFile(wp.CertificateLocation) + assert.Nil(t, err) + + certsList, err := certs.GetAllCerts(certsPem) + assert.Nil(t, err) + assert.Equal(t, 2, len(certsList)) + + // cleanup + os.Unsetenv(constants.SNYK_CA_CERTIFICATE_LOCATION_ENV) + os.Remove(file.Name()) +} diff --git a/cliv2/pkg/basic_workflows/legacycli.go b/cliv2/pkg/basic_workflows/legacycli.go index 750a92450b..9d02ea0db0 100644 --- a/cliv2/pkg/basic_workflows/legacycli.go +++ b/cliv2/pkg/basic_workflows/legacycli.go @@ -84,10 +84,10 @@ func legacycliWorkflow(invocation workflow.InvocationContext, input []workflow.D // init proxy object wrapperProxy, err := proxy.NewWrapperProxy(insecure, cacheDirectory, cliv2.GetFullVersion(), debugLogger) - defer wrapperProxy.Close() if err != nil { return output, errors.Wrap(err, "Failed to create proxy!") } + defer wrapperProxy.Close() wrapperProxy.SetUpstreamProxyAuthentication(proxyAuthenticationMechanism) diff --git a/test/acceptance/fake-server.ts b/test/acceptance/fake-server.ts index 6fe7f5b8ef..02f87a1004 100644 --- a/test/acceptance/fake-server.ts +++ b/test/acceptance/fake-server.ts @@ -491,6 +491,10 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => { res.status(200).send({}); }); + app.post(basePath.replace('v1', 'hidden') + '/orgs/:org/sbom', (req, res) => { + res.status(200).send({}); + }); + app.get(basePath + '/download/driftctl', (req, res) => { const fixturePath = getFixturePath('iac'); const path1 = path.join(fixturePath, 'drift', 'download-test.sh'); diff --git a/test/jest/acceptance/extra-certs.spec.ts b/test/jest/acceptance/extra-certs.spec.ts new file mode 100644 index 0000000000..cc2e423090 --- /dev/null +++ b/test/jest/acceptance/extra-certs.spec.ts @@ -0,0 +1,115 @@ +import { runSnykCLI } from '../util/runSnykCLI'; +import * as fs from 'fs'; +import { runCommand } from '../util/runCommand'; +import { fakeServer } from '../../../test/acceptance/fake-server'; +import { isCLIV2 } from '../util/isCLIV2'; + +if (isCLIV2()) { + console.debug('isCLIV2'); +} + +jest.setTimeout(1000 * 60 * 1); +describe('Extra CA certificates specified with `NODE_EXTRA_CA_CERTS`', () => { + it('using a not existing file', async () => { + const { code } = await runSnykCLI(`woof --debug`, { + env: { + ...process.env, + NODE_EXTRA_CA_CERTS: 'doesntexist.crt', + }, + }); + + expect(code).toBe(0); + }); + + it('using an invalid file', async () => { + const filename = 'someotherfile.txt'; + const writeStream = fs.createWriteStream(filename); + writeStream.write('Hello World'); + writeStream.end(); + + const { code } = await runSnykCLI(`woof --debug`, { + env: { + ...process.env, + NODE_EXTRA_CA_CERTS: filename, + }, + }); + + expect(code).toBe(0); + fs.unlink(filename, () => {}); + }); + + it('using a valid cert file', async () => { + // generate certificate + const res = await runCommand( + 'go', + ['run', 'cmd/make-cert/main.go', 'mytestcert'], + { cwd: 'cliv2', env: { ...process.env, SNYK_DNS_NAMES: 'localhost' } }, + ); + + console.debug(res.stderr); + expect(res.code).toBe(0); + + // setup https server + const port = 2132; + const token = '1234'; + const baseApi = '/api/v1'; + const SNYK_API = 'https://localhost:' + port + baseApi; + const server = fakeServer(baseApi, token); + const certPem = fs.readFileSync('cliv2/mytestcert.pem'); + const keyPem = fs.readFileSync('cliv2/mytestcert.key'); + + await server.listenWithHttps(port, { cert: certPem, key: keyPem }); + + // invoke WITHOUT additional certificate set => fails + const res1 = await runSnykCLI(`test --debug`, { + env: { + ...process.env, + SNYK_API: SNYK_API, + SNYK_TOKEN: token, + }, + }); + + // invoke WITH additional certificate set => succeeds + const res2 = await runSnykCLI(`test --debug`, { + env: { + ...process.env, + NODE_EXTRA_CA_CERTS: 'cliv2/mytestcert.crt', + SNYK_API: SNYK_API, + SNYK_TOKEN: token, + }, + }); + + let res3 = { code: 2 }; + let res4 = { code: 0 }; + if (isCLIV2()) { + // invoke WITHOUT additional certificate set => succeeds + res3 = await runSnykCLI(`sbom --experimental --debug --org 1234`, { + env: { + ...process.env, + SNYK_API: SNYK_API, + SNYK_TOKEN: token, + }, + }); + + // invoke WITH additional certificate set => succeeds + res4 = await runSnykCLI(`sbom --experimental --debug --org 1234`, { + env: { + ...process.env, + NODE_EXTRA_CA_CERTS: 'cliv2/mytestcert.crt', + SNYK_API: SNYK_API, + SNYK_TOKEN: token, + }, + }); + } + + await server.closePromise(); + + expect(res1.code).toBe(2); + expect(res2.code).toBe(0); + expect(res3.code).toBe(2); + expect(res4.code).toBe(0); + fs.unlink('cliv2/mytestcert.crt', () => {}); + fs.unlink('cliv2/mytestcert.key', () => {}); + fs.unlink('cliv2/mytestcert.pem', () => {}); + }); +});