├── .github └── workflows │ ├── gitleaks.yml │ └── gosec.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── TODO.todo ├── bootstrap.sh ├── config ├── envoy.json ├── gateway.json └── nats-server.conf ├── demo-clients ├── java-gradle │ ├── .gitattributes │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── demo │ │ │ │ └── client │ │ │ │ └── App.java │ │ │ └── test │ │ │ └── java │ │ │ └── demo │ │ │ └── client │ │ │ └── AppTest.java │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle └── python-pypi │ ├── README.md │ ├── requirements.txt │ └── setup.sh ├── docker-compose.yml ├── docs ├── Configuration-Management.md ├── Gateway-Access.md ├── Gateway-Authentication.md ├── Gateway-Observability.md ├── auth-flow.plantuml ├── build.sh ├── data-plane-flow.plantuml ├── domain.plantuml └── images │ ├── auth-flow.png │ ├── data-plane-flow.png │ ├── domain.png │ └── supply-chain-gateway-hld.png ├── pacman ├── README.md ├── data │ ├── maven-settings.xml │ └── plugin.gradle ├── lib │ ├── clean.sh │ ├── configuration.sh │ ├── conventions.sh │ ├── gradle.sh │ ├── maven.sh │ └── utils.sh └── pacman.sh ├── pki └── EMPTY ├── policies ├── Makefile ├── data.json ├── example.rego └── example_test.rego └── services ├── .dockerignore ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── Makefile ├── cmd ├── confli │ ├── README.md │ ├── confli.go │ ├── envoy_generator.go │ └── sample_generator.go ├── dcs │ └── dcs.go ├── pdp │ └── pdp.go ├── pds │ └── pds.go └── tap │ └── tap.go ├── go.mod ├── go.sum ├── pkg ├── auth │ ├── auth_credential.go │ ├── auth_credential_test.go │ ├── auth_identity.go │ ├── authentication.go │ ├── basic_auth.go │ ├── envoy_adapter.go │ ├── noauth-auth.go │ ├── provider.go │ └── utils.go ├── common │ ├── adapters │ │ ├── grpc.go │ │ └── messaging.go │ ├── config │ │ ├── config.go │ │ ├── feature.go │ │ ├── repository.go │ │ ├── repository_file.go │ │ └── translator.go │ ├── db │ │ ├── adapters │ │ │ ├── mysql_adapter.go │ │ │ └── sql_adapter.go │ │ ├── migration.go │ │ ├── models │ │ │ └── vulnerability.go │ │ └── repository.go │ ├── logger │ │ └── logger.go │ ├── messaging │ │ ├── messaging.go │ │ ├── messaging_kafka_protobuf.go │ │ └── messaging_nats.go │ ├── models │ │ ├── artefact_utils.go │ │ ├── event_utils.go │ │ ├── events.go │ │ ├── models.go │ │ ├── upstream_utils.go │ │ └── upstream_utils_test.go │ ├── obs │ │ └── tracing.go │ ├── openssf │ │ ├── osv.go │ │ ├── osv_adapter.go │ │ └── scorecard.go │ ├── route │ │ ├── route.go │ │ └── route_test.go │ └── utils │ │ ├── strings.go │ │ ├── tls.go │ │ └── uid.go ├── dcs │ ├── data_service.go │ ├── dispatcher.go │ ├── sbom.go │ └── vulnerability.go ├── pdp │ ├── auth.go │ ├── authorizer.go │ ├── extended_context.go │ ├── pds_client.go │ ├── pds_client_local.go │ ├── pds_client_raya.go │ ├── policy_engine.go │ ├── policy_model.go │ ├── policy_model_utils.go │ └── utils.go ├── pds │ ├── openssf_vuln_wrap.go │ ├── policy_data_service.go │ └── vulnerability_model_wrap.go ├── secrets │ ├── provider.go │ ├── provider_env.go │ └── secret.go └── tap │ ├── tap_handler_event_pub.go │ ├── tap_model.go │ ├── tap_response.go │ ├── tap_service.go │ ├── tap_upstream_auth.go │ └── tap_utils.go └── spec ├── openssf └── osv-api-openapi.yml └── proto ├── config.proto ├── events.proto ├── lib └── google │ └── api │ ├── annotations.proto │ └── http.proto ├── models.proto ├── pds.proto └── raya.proto /.github/workflows/gitleaks.yml: -------------------------------------------------------------------------------- 1 | name: Secrets Scan 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | trufflehog: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Source 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: '0' 14 | - name: TruffleHog OSS 15 | uses: trufflesecurity/trufflehog@main 16 | with: 17 | path: ./ 18 | base: main 19 | head: HEAD 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/gosec.yml: -------------------------------------------------------------------------------- 1 | name: Run Gosec 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | gosec: 11 | runs-on: ubuntu-latest 12 | env: 13 | GO111MODULE: on 14 | steps: 15 | - name: Checkout Source 16 | uses: actions/checkout@v2 17 | - name: Run Gosec Security Scanner 18 | uses: securego/gosec@master 19 | with: 20 | args: ./services/... 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/.cache 2 | pki/ 3 | .env 4 | config/gateway-auth-basic.txt 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "services/spec/proto/lib/protoc-gen-validate"] 2 | path = services/spec/proto/lib/protoc-gen-validate 3 | url = https://github.com/envoyproxy/protoc-gen-validate 4 | -------------------------------------------------------------------------------- /TODO.todo: -------------------------------------------------------------------------------- 1 | Gateway Core: 2 | ✔ Adopt envoy proxy as the gateway @done(22-04-18 07:50) 3 | ✔ Support path based routing to upstreams @done(22-04-19 08:23) 4 | ☐ Support upstream authentication with SDS 5 | ☐ Support domain based routing 6 | ☐ Support dynamic configuration with xDS 7 | 8 | Control Plane: 9 | ☐ Define a common configuration schema for all components 10 | ☐ Build a cli tool to generate service specific config from common config 11 | 12 | Data Plane: 13 | ✔ Implement PDP and integrate with Envoy using ExtAuthZ @done(22-04-18 07:55) 14 | ✔ Implement Tap and integrate with Envoy for event publishing @done(22-04-19 18:07) 15 | ✔ Adopt a messaging service and publish events @done(22-04-23 09:55) 16 | 17 | Policy Management: 18 | ✔ Integrate OPA as policy engine @done(22-04-18 07:56) 19 | ✔ Finalise the policy input schema @done(22-04-18 07:56) 20 | ✔ Implement policy evaluatioin on artefact model @done(22-04-18 21:10) 21 | ☐ Enhance time based policy reload to use inotify/kqueue 22 | 23 | Data Collectors: 24 | ✔ Implement vulnerability collection for artefacts @done(22-04-28 20:28) 25 | ☐ Implement license meta-data collection for artefacts 26 | ☐ Implement SBOM generator through TAP events 27 | 28 | Policy Data Service: 29 | ✔ Finalise database technology to use @done(22-04-28 20:28) 30 | ✔ Finalise the database schema @done(22-04-28 20:28) 31 | ✔ Implement query API (gRPC) @done(22-05-06 12:33) 32 | 33 | Admin Service: 34 | ☐ RTFM and finalise the control plane architecture for Envoy, PDP, Tap etc. 35 | ☐ Admin Service OpenAPI spec 36 | ☐ Why separate config service? Why not have admin service directly do config injection? 37 | 38 | Other wishlist: 39 | ☐ Artefact provenance verification as per SLSA framework 40 | ✔ mTLS for all internal communication @done(22-05-06 12:33) 41 | ☐ Dependency confusion attack mitigation 42 | ☐ Hot reload of all service configuration from a config server (etcd?) 43 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | export ROOT_SUBJ="/C=IN/ST=KA/L=Bangalore/O=WeekendLabs/OU=DevOps/CN=weekend.labs/emailAddress=bofh@dev.null" 6 | 7 | openssl req -nodes -new -x509 \ 8 | -keyout pki/root.key -out pki/root.crt \ 9 | -subj $ROOT_SUBJ 10 | 11 | export PARTICIPATING_SERVICES="pdp tap dcs pds nats-server" 12 | 13 | for svc in $PARTICIPATING_SERVICES; do 14 | mkdir -p pki/$svc 15 | 16 | openssl genrsa -out pki/$svc/server.key 2048 17 | 18 | openssl req -new -sha256 -key pki/$svc/server.key \ 19 | -subj "/C=IN/ST=KA/O=WeekendLabs/CN=$svc" \ 20 | -addext "subjectAltName=DNS:$svc" \ 21 | -out pki/$svc/server.csr 22 | 23 | openssl x509 -req -in pki/$svc/server.csr \ 24 | -CA pki/root.crt -CAkey pki/root.key -CAcreateserial \ 25 | --extensions v3_req \ 26 | -extfile <(printf "[v3_req]\nsubjectAltName=DNS:$svc") \ 27 | -out pki/$svc/server.crt -days 30 -sha256 28 | done 29 | 30 | # This is insecure but is needed for docker-compose 31 | # In a production environment, we must use a cert manager instead of 32 | # manually generating certificates 33 | 34 | find ./pki -type f -exec chmod 644 {} \; 35 | 36 | if [ ! -f ".env" ]; then 37 | # Generate secrets 38 | mysql_root_pass=$(openssl rand -hex 32) 39 | cat > .env <<_EOF 40 | MYSQL_ROOT_PASSWORD=$mysql_root_pass 41 | MYSQL_DCS_DATABASE=vdb 42 | MYSQL_DCS_USER=root 43 | MYSQL_DCS_PASSWORD=$mysql_root_pass 44 | 45 | KAFKA_PONGO_HOST=127.0.0.1 46 | _EOF 47 | fi 48 | -------------------------------------------------------------------------------- /config/gateway.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "id": "01GFJE3RN787D1NGW4VJEQ07HT", 4 | "name": "localhost", 5 | "domain": "localhost" 6 | }, 7 | "listener": { 8 | "host": "0.0.0.0", 9 | "port": 10000 10 | }, 11 | "upstreams": [ 12 | { 13 | "type": "Maven", 14 | "managementType": "GatewayAdmin", 15 | "name": "maven-central", 16 | "authentication": { 17 | "type": "Basic", 18 | "provider": "default-basic-auth" 19 | }, 20 | "route": { 21 | "pathPrefix": "/maven2", 22 | "hostRewriteValue": "repo.maven.apache.org", 23 | "pathPrefixRewriteValue": "/maven2" 24 | }, 25 | "repository": { 26 | "host": "repo.maven.apache.org", 27 | "port": "443", 28 | "tls": true, 29 | "sni": "repo.maven.apache.org", 30 | "authentication": { 31 | 32 | } 33 | } 34 | }, 35 | { 36 | "type": "Maven", 37 | "managementType": "GatewayAdmin", 38 | "name": "gradle-plugins", 39 | "authentication": { 40 | "type": "Basic", 41 | "provider": "default-basic-auth" 42 | }, 43 | "route": { 44 | "pathPrefix": "/gradle-plugins/m2", 45 | "hostRewriteValue": "plugins.gradle.org", 46 | "pathPrefixRewriteValue": "/m2" 47 | }, 48 | "repository": { 49 | "host": "plugins.gradle.org", 50 | "port": "443", 51 | "tls": true, 52 | "sni": "plugins.gradle.org", 53 | "authentication": { 54 | 55 | } 56 | } 57 | }, 58 | { 59 | "type": "PyPI", 60 | "managementType": "GatewayAdmin", 61 | "name": "pypi_org", 62 | "authentication": { 63 | "type": "Basic", 64 | "provider": "default-basic-auth" 65 | }, 66 | "route": { 67 | "pathPrefix": "/pypi", 68 | "hostRewriteValue": "pypi.org", 69 | "pathPrefixRewriteValue": "/pypi" 70 | }, 71 | "repository": { 72 | "host": "pypi.org", 73 | "port": "443", 74 | "tls": true, 75 | "sni": "pypi.org", 76 | "authentication": { 77 | 78 | } 79 | } 80 | } 81 | ], 82 | "authenticators": { 83 | "default-basic-auth": { 84 | "type": "Basic", 85 | "basicAuth": { 86 | "path": "/auth/basic-auth-credentials.txt" 87 | } 88 | } 89 | }, 90 | "messaging": { 91 | "kafka": { 92 | "type": "KAFKA", 93 | "kafka": { 94 | "bootstrapServers": [ 95 | "kafka-host:9092" 96 | ], 97 | "schemaRegistryUrl": "http://kafka-host:8081" 98 | } 99 | }, 100 | "nats": { 101 | "nats": { 102 | "url": "tls://nats-server:4222" 103 | } 104 | } 105 | }, 106 | "services": { 107 | "pdp": { 108 | "monitorMode": true, 109 | "pdsClient": { 110 | "common": { 111 | "host": "pds", 112 | "port": 9002, 113 | "mtls": true 114 | } 115 | }, 116 | "publisherConfig": { 117 | "messagingAdapterName": "nats", 118 | "topicNames": { 119 | "policyAudit": "gateway.pdp.audits" 120 | } 121 | } 122 | }, 123 | "tap": { 124 | "publisherConfig": { 125 | "messagingAdapterName": "nats", 126 | "topicNames": { 127 | "upstreamRequest": "gateway.tap.upstream_req", 128 | "upstreamResponse": "gateway.tap.upstream_res" 129 | } 130 | } 131 | }, 132 | "dcs": { 133 | "active": true, 134 | "messagingAdapterName": "nats" 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /config/nats-server.conf: -------------------------------------------------------------------------------- 1 | port: 4222 2 | monitor_port: 8222 3 | 4 | trace: true 5 | debug: true 6 | 7 | tls { 8 | cert_file: "/config/pki/server.crt" 9 | key_file: "/config/pki/server.key" 10 | ca_file: "/config/pki/root.crt" 11 | verify: true 12 | } 13 | -------------------------------------------------------------------------------- /demo-clients/java-gradle/.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /demo-clients/java-gradle/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | -------------------------------------------------------------------------------- /demo-clients/java-gradle/README.md: -------------------------------------------------------------------------------- 1 | # Demo Client 2 | 3 | Demo client using Gradle/Java and supply chain gateway as the maven central source for public library access. 4 | 5 | ## Requirements 6 | 7 | * Java / JDK 11 8 | 9 | ## Usage 10 | 11 | ```bash 12 | ./gradlew assemble --refresh-dependencies 13 | ``` 14 | 15 | ## Observations 16 | 17 | Failure to fetch vulnerable `log4j` dependency 18 | 19 | ``` 20 | > Could not resolve all files for configuration ':app:compileClasspath'. 21 | > Could not resolve org.apache.logging.log4j:log4j:2.16.0. 22 | Required by: 23 | project :app 24 | > Could not resolve org.apache.logging.log4j:log4j:2.16.0. 25 | > Could not get resource 'http://localhost:10000/maven2/org/apache/logging/log4j/log4j/2.16.0/log4j-2.16.0.pom'. 26 | > Could not GET 'http://localhost:10000/maven2/org/apache/logging/log4j/log4j/2.16.0/log4j-2.16.0.pom'. Received status code 403 from server: Forbidden 27 | ``` 28 | -------------------------------------------------------------------------------- /demo-clients/java-gradle/app/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This generated file contains a sample Java application project to get you started. 5 | * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle 6 | * User Manual available at https://docs.gradle.org/7.3.3/userguide/building_java_projects.html 7 | */ 8 | 9 | plugins { 10 | // Apply the application plugin to add support for building a CLI application in Java. 11 | id 'application' 12 | id 'org.openapi.generator' version '5.4.0' 13 | } 14 | 15 | repositories { 16 | // Use supply chain gateway for external repository access 17 | maven { 18 | url "http://localhost:10000/maven2" 19 | allowInsecureProtocol true 20 | 21 | // Credentials are not verified currently but we want to 22 | // support multiple pluggable IDPs for easy integration with 23 | // various CI 24 | credentials { 25 | authentication { 26 | basic(BasicAuthentication) 27 | } 28 | 29 | username "demo-java-gradle/someUser@someOrg" 30 | password "somePassword" 31 | } 32 | } 33 | } 34 | 35 | dependencies { 36 | // Use JUnit test framework. 37 | testImplementation 'junit:junit:4.13.2' 38 | 39 | // This dependency is used by the application. 40 | implementation 'com.google.guava:guava:30.1.1-jre' 41 | implementation 'org.apache.commons:commons-collections4:4.1' 42 | implementation 'org.springframework.boot:spring-boot-starter-web:2.4.6' 43 | 44 | // Vulnerable dependency to trigger example policy 45 | implementation 'org.apache.logging.log4j:log4j:2.16.0' 46 | 47 | // Dependency confusion test 48 | implementation group: 'org.example', name: 'junit-utils', version: '1.0.6.RELEASE' 49 | } 50 | 51 | application { 52 | // Define the main class for the application. 53 | mainClass = 'demo.client.App' 54 | } 55 | -------------------------------------------------------------------------------- /demo-clients/java-gradle/app/src/main/java/demo/client/App.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Java source file was generated by the Gradle 'init' task. 3 | */ 4 | package demo.client; 5 | 6 | public class App { 7 | public String getGreeting() { 8 | return "Hello World!"; 9 | } 10 | 11 | public static void main(String[] args) { 12 | System.out.println(new App().getGreeting()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demo-clients/java-gradle/app/src/test/java/demo/client/AppTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Java source file was generated by the Gradle 'init' task. 3 | */ 4 | package demo.client; 5 | 6 | import org.junit.Test; 7 | import static org.junit.Assert.*; 8 | 9 | public class AppTest { 10 | @Test public void appHasAGreeting() { 11 | App classUnderTest = new App(); 12 | assertNotNull("app should have a greeting", classUnderTest.getGreeting()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demo-clients/java-gradle/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhisek/supply-chain-security-gateway/a9073a81927920ae18ad873ec968a82891668601/demo-clients/java-gradle/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo-clients/java-gradle/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /demo-clients/java-gradle/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /demo-clients/java-gradle/settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/7.3.3/userguide/multi_project_builds.html 8 | */ 9 | 10 | pluginManagement { 11 | repositories { 12 | maven { 13 | url "http://localhost:10000/gradle-plugins/m2" 14 | allowInsecureProtocol true 15 | 16 | // Credentials are not verified currently but we want to 17 | // support multiple pluggable IDPs for easy integration with 18 | // various CI 19 | credentials { 20 | authentication { 21 | basic(BasicAuthentication) 22 | } 23 | 24 | username "demo-java-gradle/someUser@someOrg" 25 | password "somePassword" 26 | } 27 | } 28 | 29 | maven { 30 | url "http://localhost:10000/maven2" 31 | allowInsecureProtocol true 32 | 33 | // Credentials are not verified currently but we want to 34 | // support multiple pluggable IDPs for easy integration with 35 | // various CI 36 | credentials { 37 | authentication { 38 | basic(BasicAuthentication) 39 | } 40 | 41 | username "demo-java-gradle/someUser@someOrg" 42 | password "somePassword" 43 | } 44 | } 45 | } 46 | } 47 | 48 | rootProject.name = 'demo-client' 49 | include('app') 50 | -------------------------------------------------------------------------------- /demo-clients/python-pypi/README.md: -------------------------------------------------------------------------------- 1 | # Python Demo Client 2 | 3 | ## Usage 4 | 5 | Run `setup.sh` to configure `pip` to use security gateway as the index 6 | 7 | ```bash 8 | ./setup.sh 9 | ``` 10 | 11 | Download Python artefacts using `pip` 12 | 13 | ```bash 14 | pip3 install -r ./requirements.txt 15 | ``` 16 | 17 | > Pypi support is incomplete due to challenges with resolving version from client requests 18 | -------------------------------------------------------------------------------- /demo-clients/python-pypi/requirements.txt: -------------------------------------------------------------------------------- 1 | awscli==1.23.3 2 | botocore==1.25.3 3 | colorama==0.4.4 4 | docutils==0.15.2 5 | jmespath==0.10.0 6 | pyasn1==0.4.8 7 | python-dateutil==2.8.2 8 | PyYAML==5.4.1 9 | rsa==4.7.2 10 | s3transfer==0.5.2 11 | six==1.16.0 12 | urllib3==1.26.9 13 | -------------------------------------------------------------------------------- /demo-clients/python-pypi/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m pip config --user set global.index http://localhost:10000/pypi 4 | python3 -m pip config --user set global.index-url http://localhost:10000/pypi/simple 5 | python3 -m pip config --user set global.trusted-host localhost 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | envoy: 4 | image: envoyproxy/envoy:v1.21.1 5 | command: envoy -c /config/envoy.json 6 | volumes: 7 | - ${BOOTSTRAP_ENVOY_FILE:-./config/envoy.json}:/config/envoy.json 8 | ports: 9 | - "10000:10000" 10 | mysql-server: 11 | image: mysql:8.0 12 | volumes: 13 | - mysql-db:/var/lib/mysql 14 | environment: 15 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 16 | MYSQL_DATABASE: ${MYSQL_DCS_DATABASE} 17 | nats-server: 18 | image: nats:2.7-alpine 19 | ports: 20 | - "8222:8222" 21 | command: -c /config/server.conf 22 | volumes: 23 | - ./config/nats-server.conf:/config/server.conf 24 | - ./pki/nats-server/server.crt:/config/pki/server.crt 25 | - ./pki/nats-server/server.key:/config/pki/server.key 26 | - ./pki/root.crt:/config/pki/root.crt 27 | pdp: 28 | build: ./services 29 | command: pdp-server 30 | volumes: 31 | - ${BOOTSTRAP_CONFIG_FILE:-./config/gateway.json}:/config/gateway.json 32 | - ${BOOTSTRAP_BASIC_AUTH_FILE:-./config/gateway-auth-basic.txt}:/auth/basic-auth-credentials.txt 33 | - ./policies:/policies 34 | - ./pki/pdp/server.crt:/config/pki/server.crt 35 | - ./pki/pdp/server.key:/config/pki/server.key 36 | - ./pki/root.crt:/config/pki/root.crt 37 | environment: 38 | GLOBAL_CONFIG_PATH: /config/gateway.json 39 | PDP_POLICY_PATH: /policies 40 | SERVICE_TLS_CERT: /config/pki/server.crt 41 | SERVICE_TLS_KEY: /config/pki/server.key 42 | SERVICE_TLS_ROOT_CA: /config/pki/root.crt 43 | PDS_HOST: pds 44 | PDS_PORT: 9002 45 | PDP_KAFKA_PONGO_BOOTSTRAP_SERVERS: kafka1-host:9092 46 | PDP_KAFKA_PONGO_SCHEMA_REGISTRY_URL: http://kafka1-host:8081 47 | APP_SERVICE_OBS_ENABLED: ${PDP_SERVICE_OBS_ENABLED:-true} 48 | APP_SERVICE_NAME: ${PDP_SERVICE_NAME:-pdp} 49 | APP_SERVICE_ENV: ${PDP_SERVICE_ENV:-development} 50 | APP_OTEL_EXPORTER_OTLP_ENDPOINT: ${COMMON_OTEL_EXPORTER_OTLP_ENDPOINT:-localhost:4317} 51 | extra_hosts: 52 | kafka1-host: ${KAFKA_PONGO_HOST} 53 | tap: 54 | build: ./services 55 | command: tap-server 56 | volumes: 57 | - ${BOOTSTRAP_CONFIG_FILE:-./config/gateway.json}:/config/gateway.json 58 | - ./pki/tap/server.crt:/config/pki/server.crt 59 | - ./pki/tap/server.key:/config/pki/server.key 60 | - ./pki/root.crt:/config/pki/root.crt 61 | environment: 62 | GLOBAL_CONFIG_PATH: /config/gateway.json 63 | SERVICE_TLS_CERT: /config/pki/server.crt 64 | SERVICE_TLS_KEY: /config/pki/server.key 65 | SERVICE_TLS_ROOT_CA: /config/pki/root.crt 66 | APP_SERVICE_OBS_ENABLED: ${TAP_SERVICE_OBS_ENABLED:-true} 67 | APP_SERVICE_NAME: ${TAP_SERVICE_NAME:-tap} 68 | APP_SERVICE_ENV: ${TAP_SERVICE_ENV:-development} 69 | APP_OTEL_EXPORTER_OTLP_ENDPOINT: ${COMMON_OTEL_EXPORTER_OTLP_ENDPOINT:-localhost:4317} 70 | dcs: 71 | depends_on: 72 | - mysql-server 73 | build: ./services 74 | command: dcs-server 75 | volumes: 76 | - ${BOOTSTRAP_CONFIG_FILE:-./config/gateway.json}:/config/gateway.json 77 | - ./pki/dcs/server.crt:/config/pki/server.crt 78 | - ./pki/dcs/server.key:/config/pki/server.key 79 | - ./pki/root.crt:/config/pki/root.crt 80 | environment: 81 | GLOBAL_CONFIG_PATH: /config/gateway.json 82 | SERVICE_TLS_CERT: /config/pki/server.crt 83 | SERVICE_TLS_KEY: /config/pki/server.key 84 | SERVICE_TLS_ROOT_CA: /config/pki/root.crt 85 | MYSQL_SERVER_HOST: mysql-server 86 | MYSQL_SERVER_PORT: 3306 87 | MYSQL_DATABASE: ${MYSQL_DCS_DATABASE} 88 | MYSQL_USER: ${MYSQL_DCS_USER} 89 | MYSQL_PASSWORD: ${MYSQL_DCS_PASSWORD} 90 | APP_SERVICE_OBS_ENABLED: ${DCS_SERVICE_OBS_ENABLED:-true} 91 | APP_SERVICE_NAME: ${DCS_SERVICE_NAME:-dcs} 92 | APP_SERVICE_ENV: ${DCS_SERVICE_ENV:-development} 93 | APP_OTEL_EXPORTER_OTLP_ENDPOINT: ${COMMON_OTEL_EXPORTER_OTLP_ENDPOINT:-localhost:4317} 94 | pds: 95 | depends_on: 96 | - mysql-server 97 | - dcs 98 | build: ./services 99 | command: pds-server 100 | volumes: 101 | - ${BOOTSTRAP_CONFIG_FILE:-./config/gateway.json}:/config/gateway.json 102 | - ./pki/pds/server.crt:/config/pki/server.crt 103 | - ./pki/pds/server.key:/config/pki/server.key 104 | - ./pki/root.crt:/config/pki/root.crt 105 | environment: 106 | GLOBAL_CONFIG_PATH: /config/gateway.json 107 | PDS_SERVER_NAME: pds 108 | SERVICE_TLS_CERT: /config/pki/server.crt 109 | SERVICE_TLS_KEY: /config/pki/server.key 110 | SERVICE_TLS_ROOT_CA: /config/pki/root.crt 111 | MYSQL_SERVER_HOST: mysql-server 112 | MYSQL_SERVER_PORT: 3306 113 | MYSQL_DATABASE: ${MYSQL_DCS_DATABASE} 114 | MYSQL_USER: ${MYSQL_DCS_USER} 115 | MYSQL_PASSWORD: ${MYSQL_DCS_PASSWORD} 116 | APP_SERVICE_OBS_ENABLED: ${PDS_SERVICE_OBS_ENABLED:-true} 117 | APP_SERVICE_NAME: ${PDS_SERVICE_NAME:-pds} 118 | APP_SERVICE_ENV: ${PDS_SERVICE_ENV:-development} 119 | APP_OTEL_EXPORTER_OTLP_ENDPOINT: ${COMMON_OTEL_EXPORTER_OTLP_ENDPOINT:-localhost:4317} 120 | volumes: 121 | mysql-db: {} 122 | -------------------------------------------------------------------------------- /docs/Configuration-Management.md: -------------------------------------------------------------------------------- 1 | # Configuration Management 2 | 3 | ## Goal 4 | 5 | Adopt a spec based approach for configuration model definition that can be represented in formats such as JSON, Protobuf and can be persisted as files or in databases using an appropriate repository. Ensure configuration has a Single Source of Truth 6 | 7 | ## Who are the users of configuration management 8 | 9 | 1. Environment administrators who provision gateways 10 | 2. Gateway administrators who configure upstream, authentication, secrets etc. 11 | 3. Envoy proxy to act as the gateway and route request to upstream 12 | 4. Microservices runtime configuration 13 | 14 | ## Configuration Model 15 | 16 | The configuration model is based on the [domain model](images/domain.png) but with additional detail for service and environment related configuration. 17 | 18 | ## How to define configuration schema 19 | 20 | Look at `services/spec/config.proto` 21 | 22 | ## How to use configuration schema 23 | 24 | Use `protoc` to compile the spec and generate code as required. The underlying spec has associated validator. 25 | 26 | ## How to store the configuration 27 | 28 | Storage and service layer for configuration is required and is within the boundary of the service implementing it. The service layer exposes the configuration to management and operations (data) plane. 29 | 30 | ## Dynamic Configuration 31 | 32 | To be able to configure the gateway and associated service at runtime, each service must support dynamic configuration i.e. monitor for change in its configuration state and periodically re-configure itself if the SSOT for its configuration has changed. 33 | 34 | To do so, every service must: 35 | 36 | * Have an instance identity of its own representing itself in a deployed environment 37 | * Query the configuration repository with its instance identifier 38 | * Update its in-memory configuration 39 | 40 | > **Note:** There are services that are not dynamically configurable such as queue/topic listeners. 41 | 42 | ### Bootstrap Configuration 43 | 44 | Every service has a bootstrap configuration which is minimal and allows it to discover and access dynamic configuration source. Bootstrap configuration can be passed through environment variables: 45 | 46 | ``` 47 | BOOTSTRAP_CONFIGURATION_REPOSITORY_TYPE=file 48 | BOOTSTRAP_CONFIGURATION_REPOSITORY_PATH=/path/to/config.json 49 | ``` 50 | 51 | or using following environment variable which implicitly assumes file based config repository 52 | 53 | ``` 54 | GLOBAL_CONFIG_PATH=/path/to/gateway.json 55 | ``` 56 | 57 | ### Configuration API 58 | 59 | The `config` common module should be initialized and subsequently can be used for obtaining currently loaded configurations: 60 | 61 | ```go 62 | if config.PdpServiceConfig.MonitorMode() { 63 | // Do something 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/Gateway-Access.md: -------------------------------------------------------------------------------- 1 | # Gateway Access 2 | 3 | The security gateway can be accessed as any HTTP based package repository such as `maven2`, `npm`, `rubygems` etc. However package managers can be configured to send additional metadata to enrich the generated events for auditing and traceability purpose. 4 | 5 | ## Request Metadata 6 | 7 | Every artifact download request can have additional metadata for auditing or traceability purpose. Following metadata can be attached to a request to the gateway 8 | 9 | 1. Project ID 10 | 2. Project Environment Name 11 | 3. Labels (Generic key value pairs) 12 | 13 | Since different package managers have different capabilities of *decorating a request*, we have to support different channels through which additional metadata can be included in a request to the gateway. 14 | 15 | ### Using Headers 16 | 17 | | Name | Description | 18 | | -------------------- | -------------------------------------------------------- | 19 | | X-SGW-Project-Id | Name of the project using this artifact | 20 | | X-SGW-Project-Env | Environment name for which the project is built | 21 | | X-SGW-Project-Labels | Additional metadata in `key1=value1, key2=value2` format | 22 | 23 | ### Encoding in Username 24 | 25 | Another supported way of supplying project identifier is by encoding it in the username for accessing the gateway. In a username of form `project-id/user@org`, the string before `/` is ignored while performing authentication and is used to identify the project. So in this form, `project-id` will be included in any event generated by the gateway. 26 | -------------------------------------------------------------------------------- /docs/Gateway-Authentication.md: -------------------------------------------------------------------------------- 1 | # Gateway Authentication 2 | 3 | ## Goal 4 | 5 | Restrict gateway access to authenticated users only as per configuration. 6 | 7 | > **Note:** Gateway authentication is applicable for users trying to access the Gateway. It is not relevant for authentication requirements for upstream repositories. Upstream repositories can have authentication of their own. 8 | 9 | ## Requirements 10 | 11 | - Global authentication across all upstreams / routes in the gateway 12 | - Upstream specific authentication 13 | - Support different types of authentication 14 | - RelayAuth - Relay the credentials to upstream repository 15 | - OpenID Connect (OIDC) - To support Github Actions OIDC and equivalent CI integration 16 | - AWS STS AssumeRole 17 | 18 | ### Limitation 19 | 20 | Basic authentication as the only supported form of supplying credentials, since most package managers use basic authentication to authenticate with repositories. 21 | 22 | We will not support fine grained *authorization* because we are in the data plane. We need to be minimalist for low latency and performance. Any need for limited authorization can be handled at policy level. 23 | 24 | 25 | ## User Identification 26 | 27 | An user authenticating to the gateway is essentially using the *data plane* of the system. The username can be used to segment / namespace gateway resources in a multi-user scenario. The following convention can be followed for username: 28 | 29 | ``` 30 | projectId/username@organization 31 | ``` 32 | 33 | This is a convention using which it is possible to access the gateway while conveying information required for the gateway to associate project and organization information to a gateway generated event, such as policy violation. This is required for auditing and traceability of generated events. 34 | 35 | ## Flow 36 | 37 | ![Authentication Flow](images/auth-flow.png) 38 | -------------------------------------------------------------------------------- /docs/Gateway-Observability.md: -------------------------------------------------------------------------------- 1 | # Gateway Observability 2 | 3 | ## Goal 4 | 5 | Adopt OpenTelemetry for MELT export from gateway services. Keep collectors and APM tools decoupled from Gateway. 6 | 7 | ## Service Configuration for MELT 8 | 9 | Following environment variables can be used to configure MELT for each microservice 10 | 11 | | Environment Name | Purpose | 12 | | ------------------------------- | -------------------------------------- | 13 | | APP_SERVICE_OBS_ENABLED | True / False | 14 | | APP_SERVICE_NAME | The service name to include traces | 15 | | APP_SERVICE_ENV | The service environment name | 16 | | APP_SERVICE_LABELS | Command separate key-value pairs | 17 | | APP_OTEL_EXPORTER_OTLP_ENDPOINT | OTLP exporter GRPC endpoint (insecure) | 18 | -------------------------------------------------------------------------------- /docs/auth-flow.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title "Authentication Flow" 4 | 5 | actor Client as Client 6 | 7 | box "Data Plane" 8 | participant "Gateway" 9 | participant "PDP" 10 | participant "TAP" 11 | end box 12 | 13 | box "Upstream" 14 | participant "Upstream" 15 | end box 16 | 17 | Client -> Gateway: Access repo (e.g. Maven Central) 18 | Gateway -> PDP: Authorize 19 | PDP -> PDP: Ingress Authentication 20 | PDP -> Gateway: Allow/Deny 21 | Gateway -> TAP: Handle request 22 | TAP -> TAP: Egress Authentication 23 | TAP -> Gateway 24 | Gateway -> Upstream: Send request with authentication 25 | Upstream -> Gateway: Artefact response 26 | Gateway -> Client: Artefact response 27 | 28 | @enduml 29 | -------------------------------------------------------------------------------- /docs/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd $(dirname $0) 6 | mkdir -p .cache 7 | 8 | if [ ! -f ".cache/plantuml.jar" ]; then 9 | wget -O .cache/plantuml.jar \ 10 | https://github.com/plantuml/plantuml/releases/download/v1.2022.4/plantuml-1.2022.4.jar 11 | fi; 12 | 13 | java -jar .cache/plantuml.jar -o ./images/ *.plantuml 14 | -------------------------------------------------------------------------------- /docs/data-plane-flow.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title "Data Plane Flow" 4 | 5 | actor Client as Client 6 | 7 | box "Data Plane" 8 | participant "Gateway" 9 | participant "PDP" 10 | participant "PDS" 11 | database "DataStore" 12 | end box 13 | 14 | box "Upstream" 15 | participant "Upstream" 16 | end box 17 | 18 | Client -> Gateway: Access repo (e.g. Maven Central) 19 | Gateway -> PDP: Policy evaluation 20 | PDP -> PDS: Lookup artefact metadata 21 | PDS <-> DataStore: Lookup vulnerabilities and license information 22 | PDS -> PDP: Enriched artefact 23 | PDP -> PDP: Policy evaluation 24 | PDP -> Gateway: Policy decision 25 | 26 | alt Policy Allowed 27 | Gateway -> Upstream: Proxy to upstream 28 | Gateway -> Client: Response from upstream 29 | else Policy Denied 30 | Gateway -> Client: Policy denied request 31 | end 32 | 33 | @enduml 34 | -------------------------------------------------------------------------------- /docs/domain.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | package "Management" { 4 | object "Organization" as org 5 | object "User" as user 6 | object "Access" as adminAccess 7 | object "Access Token" as accessToken 8 | } 9 | 10 | package "Operations" { 11 | object "Gateway" as gw 12 | object "Access Rule" as gwAccessRule 13 | object "Upstream" as upstream 14 | 15 | object "Authentication" as authN 16 | object "Policy" as policy 17 | 18 | object "Route" as route 19 | object "Repository" as repository 20 | 21 | object "Path Pattern" as path 22 | } 23 | 24 | org --> user : 1-n 25 | org --> gw : 1-n 26 | 27 | org --> adminAccess : 1-n 28 | 29 | adminAccess <-- user : 1-n 30 | adminAccess <-- gw : 1-n 31 | 32 | gw --> upstream : 1-n 33 | gw --> policy : 1-n 34 | gw --> gwAccessRule : 1-n 35 | 36 | user --> accessToken : 1-n (Gateway Upstream) 37 | 38 | upstream --> route : 1-1 39 | upstream --> repository : 1-1 40 | upstream --> policy : 1-n 41 | route --> policy : 1-n 42 | 43 | route --> path : 1-n 44 | route --> authN : 1-1 45 | repository --> authN : 1-1 46 | 47 | @enduml 48 | -------------------------------------------------------------------------------- /docs/images/auth-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhisek/supply-chain-security-gateway/a9073a81927920ae18ad873ec968a82891668601/docs/images/auth-flow.png -------------------------------------------------------------------------------- /docs/images/data-plane-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhisek/supply-chain-security-gateway/a9073a81927920ae18ad873ec968a82891668601/docs/images/data-plane-flow.png -------------------------------------------------------------------------------- /docs/images/domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhisek/supply-chain-security-gateway/a9073a81927920ae18ad873ec968a82891668601/docs/images/domain.png -------------------------------------------------------------------------------- /docs/images/supply-chain-gateway-hld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhisek/supply-chain-security-gateway/a9073a81927920ae18ad873ec968a82891668601/docs/images/supply-chain-gateway-hld.png -------------------------------------------------------------------------------- /pacman/README.md: -------------------------------------------------------------------------------- 1 | # PacMan 2 | Utility to configure build tools to use security gateway as package repository. 3 | 4 | `pacman` aka. `Package Manager` inspired by the `pacman` is a tool for easily configuring various package managers such as Gradle, Maven etc. to use the security gateway for downloading required dependencies. 5 | 6 | ## Setup 7 | 8 | Run `pacman` configuration wizard 9 | 10 | ```bash 11 | ./pacman.sh configure 12 | ``` 13 | 14 | > Refer to [gateway authentication]([../README.md#authentication)) for more details on how to create gateway users. 15 | 16 | ### Configure Gradle 17 | 18 | ```bash 19 | ./pacman.sh setup-gradle 20 | ``` 21 | 22 | ### Configure Maven 23 | 24 | ```bash 25 | ./pacman.sh setup-maven 26 | ``` 27 | 28 | > **Note:** This script overwrite `$HOME/.m2/settings.xml` 29 | 30 | ### Configuring Project 31 | 32 | To configure package managers building a specific project, set environment 33 | 34 | ``` 35 | GATEWAY_PROJECT_ID=project-id 36 | ``` 37 | 38 | ## Cleanup 39 | 40 | Remove any configuration file added by `pacman` 41 | 42 | ```bash 43 | ./pacman clean 44 | ``` 45 | 46 | # Reference 47 | 48 | * https://www.google.com/logos/2010/pacman10-i.html 49 | -------------------------------------------------------------------------------- /pacman/data/maven-settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | security-gateway 5 | {{GATEWAY_USERNAME}} 6 | {{GATEWAY_PASSWORD}} 7 | 8 | 9 | 10 | X-SGW-Project-Id 11 | ${project.name} 12 | 13 | 14 | X-SGW-Project-Env 15 | ${env.SGW_PROJECT_ENV} 16 | 17 | 18 | X-SGW-Project-Labels 19 | ${env.SGW_PROJECT_ENV} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | security-gateway 29 | * 30 | {{GATEWAY_MAVEN_CENTRAL_URL}} 31 | 32 | 33 | 34 | 35 | 36 | security-gateway 37 | 38 | 39 | central 40 | http://central 41 | true 42 | true 43 | 44 | 45 | 46 | 47 | 48 | central 49 | http://central 50 | true 51 | true 52 | 53 | 54 | 55 | 56 | 57 | 58 | security-gateway 59 | 60 | 61 | -------------------------------------------------------------------------------- /pacman/data/plugin.gradle: -------------------------------------------------------------------------------- 1 | // https://docs.gradle.org/current/userguide/init_scripts.html 2 | // TODO: Enforce gateway as the only repository URL 3 | 4 | settingsEvaluated { settings -> 5 | settings.pluginManagement { 6 | repositories { 7 | maven { 8 | authentication { 9 | basic(BasicAuthentication) 10 | } 11 | 12 | url "{{GATEWAY_GRADLE_PLUGIN_URL}}" 13 | credentials { 14 | username "{{GATEWAY_USERNAME}}" 15 | password "{{GATEWAY_PASSWORD}}" 16 | } 17 | } 18 | } 19 | } 20 | } 21 | 22 | allprojects { 23 | buildscript { 24 | repositories { 25 | maven { 26 | authentication { 27 | basic(BasicAuthentication) 28 | } 29 | 30 | url "{{GATEWAY_MAVEN_CENTRAL_URL}}" 31 | credentials { 32 | username "{{GATEWAY_USERNAME}}" 33 | password "{{GATEWAY_PASSWORD}}" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | allprojects { 41 | repositories { 42 | maven { 43 | authentication { 44 | basic(BasicAuthentication) 45 | } 46 | 47 | url "{{GATEWAY_MAVEN_CENTRAL_URL}}" 48 | credentials { 49 | username "{{GATEWAY_USERNAME}}" 50 | password "{{GATEWAY_PASSWORD}}" 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pacman/lib/clean.sh: -------------------------------------------------------------------------------- 1 | function remove_file() { 2 | file=$1 3 | 4 | echo "[WARN] Removing file: $file" 5 | rm -f $file 6 | } 7 | 8 | remove_file "$HOME/$GRADLE_INIT_SCRIPT_PATH" 9 | remove_file "$HOME/.m2/settings.xml" 10 | -------------------------------------------------------------------------------- /pacman/lib/configuration.sh: -------------------------------------------------------------------------------- 1 | configurationPath="$HOME/.scs/pacman.env" 2 | 3 | function loadConfigurationIfPresent() { 4 | if [ -f "$configurationPath" ]; then 5 | print_msg "Loading config from $configurationPath" 6 | source $configurationPath 7 | fi; 8 | } 9 | 10 | function interactiveConfiguration() { 11 | print_msg "Running interactive configuration" 12 | echo -n "Gateway Base URL (without trailing /): " 13 | read -r gatewayURL 14 | echo -n "Username: " 15 | read -r username 16 | echo -n "Password: " 17 | read -r password 18 | 19 | mkdir -p `dirname $configurationPath` 2>/dev/null 20 | cat > $configurationPath <<_EOF 21 | GATEWAY_URL=$gatewayURL 22 | GATEWAY_USERNAME=$username 23 | GATEWAY_PASSWORD=$password 24 | _EOF 25 | } 26 | -------------------------------------------------------------------------------- /pacman/lib/conventions.sh: -------------------------------------------------------------------------------- 1 | export MAVEN_CENTRAL_ROUTE="/maven2" 2 | export GRADLE_PLUGINS_ROUTE="/gradle-plugins/m2" 3 | 4 | export GRADLE_INIT_SCRIPT_PATH=".gradle/init.d/security-gateway.gradle" 5 | -------------------------------------------------------------------------------- /pacman/lib/gradle.sh: -------------------------------------------------------------------------------- 1 | gradleScriptSrc="$BASEDIR/data/plugin.gradle" 2 | gradleScriptDst="$HOME/$GRADLE_INIT_SCRIPT_PATH" 3 | 4 | function setupGradle() { 5 | gatewayURL=$1 6 | username=$2 7 | password=$3 8 | 9 | gatewayPluginPortalURL="$gatewayURL$GRADLE_PLUGINS_ROUTE" 10 | gatewayMavenCentralURL="$gatewayURL$MAVEN_CENTRAL_ROUTE" 11 | 12 | mkdir -p `dirname $gradleScriptDst` 2>/dev/null 13 | cat $gradleScriptSrc | \ 14 | sed "s,{{GATEWAY_USERNAME}},$username," | \ 15 | sed "s,{{GATEWAY_PASSWORD}},$password," | \ 16 | sed "s,{{GATEWAY_GRADLE_PLUGIN_URL}},$gatewayPluginPortalURL," | \ 17 | sed "s,{{GATEWAY_MAVEN_CENTRAL_URL}},$gatewayMavenCentralURL," \ 18 | > $gradleScriptDst 19 | } 20 | 21 | setupGradle $GATEWAY_URL $GATEWAY_USERNAME $GATEWAY_PASSWORD 22 | -------------------------------------------------------------------------------- /pacman/lib/maven.sh: -------------------------------------------------------------------------------- 1 | mavenScriptSrc="$BASEDIR/data/maven-settings.xml" 2 | mavenScriptDst="$HOME/.m2/settings.xml" 3 | 4 | function setupMaven() { 5 | gatewayURL=$1 6 | username=$2 7 | password=$3 8 | 9 | gatewayMavenCentralURL="$gatewayURL$MAVEN_CENTRAL_ROUTE" 10 | mkdir -p `dirname $mavenScriptDst` 2>/dev/null 11 | 12 | if [ -f "$mavenScriptDst" ]; then 13 | warn_msg "Overwriting $mavenScriptDst" 14 | fi; 15 | 16 | cat $mavenScriptSrc | \ 17 | sed "s,{{GATEWAY_USERNAME}},$username," | \ 18 | sed "s,{{GATEWAY_PASSWORD}},$password," | \ 19 | sed "s,{{GATEWAY_MAVEN_CENTRAL_URL}},$gatewayMavenCentralURL," \ 20 | > $mavenScriptDst 21 | } 22 | 23 | setupMaven $GATEWAY_URL $GATEWAY_USERNAME $GATEWAY_PASSWORD 24 | -------------------------------------------------------------------------------- /pacman/lib/utils.sh: -------------------------------------------------------------------------------- 1 | function error_msg() { 2 | echo "[-] ERROR: $1" 3 | } 4 | 5 | function print_msg() { 6 | echo "[*] $1" 7 | } 8 | 9 | function warn_msg() { 10 | echo "[*] WARN: $1" 11 | } 12 | -------------------------------------------------------------------------------- /pacman/pacman.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASEDIR="`dirname $0`" 4 | LIBDIR="$BASEDIR/lib" 5 | 6 | function loadScript() { 7 | scriptPath="$LIBDIR/$1.sh" 8 | if [ ! -f $scriptPath ]; then 9 | echo "** Failed to load: $scriptPath" 10 | else 11 | source $scriptPath 12 | fi; 13 | } 14 | 15 | function print_usage() { 16 | printf "Usage: $0 [arguments]\n" 17 | printf "\n" 18 | printf "Arguments:\n" 19 | printf "\t configure\n" 20 | printf "\t setup-gradle\n" 21 | printf "\t setup-maven\n" 22 | printf "\t clean\n" 23 | printf "\n" 24 | printf "Environment variables:\n" 25 | printf "\t GATEWAY_URL: The base URL of the security gateway without trailing /\n" 26 | printf "\t GATEWAY_USERNAME: Username to authenticate with gateway\n" 27 | printf "\t GATEWAY_PASSWORD: Password to authenticate with gateway\n" 28 | printf "\n" 29 | } 30 | 31 | function validateRunningEnv() { 32 | if [ -z "$GATEWAY_URL" ]; then 33 | error_msg "Gateway URL is missing" 34 | exit -1 35 | fi; 36 | 37 | if [ -z "$GATEWAY_USERNAME" ]; then 38 | error_msg "Gateway Username is missing" 39 | exit -1 40 | fi; 41 | 42 | if [ -z "$GATEWAY_PASSWORD" ]; then 43 | error_msg "Gateway Password is missing" 44 | exit -1 45 | fi; 46 | } 47 | 48 | function applyOverrides() { 49 | if [ "$GATEWAY_USERNAME" != "" ] && [ "$GATEWAY_PROJECT_ID" != "" ]; then 50 | print_msg "Applying projectId:$GATEWAY_PROJECT_ID in username" 51 | export GATEWAY_USERNAME="$GATEWAY_PROJECT_ID/$GATEWAY_USERNAME" 52 | fi 53 | } 54 | 55 | function main() { 56 | loadScript "utils" 57 | loadScript "conventions" 58 | loadScript "configuration" 59 | 60 | command=$1 61 | if [ -z "$command" ]; then 62 | print_usage 63 | exit -1 64 | fi; 65 | 66 | loadConfigurationIfPresent 67 | applyOverrides 68 | case $command in 69 | configure) 70 | interactiveConfiguration 71 | ;; 72 | setup-gradle) 73 | validateRunningEnv 74 | loadScript "gradle" 75 | ;; 76 | setup-maven) 77 | validateRunningEnv 78 | loadScript "maven" 79 | ;; 80 | clean) 81 | loadScript "clean" 82 | ;; 83 | *) 84 | error_msg "Unknown command: $command" 85 | ;; 86 | esac 87 | } 88 | 89 | main $@ 90 | -------------------------------------------------------------------------------- /pki/EMPTY: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abhisek/supply-chain-security-gateway/a9073a81927920ae18ad873ec968a82891668601/pki/EMPTY -------------------------------------------------------------------------------- /policies/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | opa test . -v 4 | 5 | -------------------------------------------------------------------------------- /policies/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "UNACCEPTABLE_VULNERABILITIES": [ 3 | "CRITICAL", "HIGH" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /policies/example.rego: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | default allow = false 4 | 5 | allow { 6 | count(violations) == 0 7 | } 8 | 9 | violations[{"message": msg, "code": code}] { 10 | input.kind != "PolicyInput" 11 | 12 | msg := "Input kind is unexpected in policy" 13 | code := 1000 14 | } 15 | 16 | violations[{"message": msg, "code": code}] { 17 | input.version.major != 1 18 | 19 | msg := "Input schema is not supported" 20 | code := 1001 21 | } 22 | 23 | violations[{"message": msg, "code": code}] { 24 | input.target.artefact.group = "org.apache.logging.log4j" 25 | input.target.artefact.name = "log4j" 26 | semver.compare(input.target.artefact.version, "2.17.0") = -1 27 | 28 | msg := "Old and vulnerable version of log4j2 is not allowed" 29 | code := 1002 30 | } 31 | 32 | violations[{"message": msg, "code": code}] { 33 | some i, j 34 | 35 | input.target.vulnerabilities[i].severity = 36 | data.UNACCEPTABLE_VULNERABILITIES[j] 37 | 38 | msg := sprintf("Vulnerabilities with %v severity blocked", 39 | [data.UNACCEPTABLE_VULNERABILITIES]) 40 | code := 1003 41 | } 42 | 43 | violations[{"message": msg, "code": code}] { 44 | input.target.artefact.source.type = "maven2" 45 | glob.match("org.example.**", ["."], input.target.artefact.group) 46 | 47 | msg := "Private namespace lookup denied" 48 | code := 1004 49 | } 50 | -------------------------------------------------------------------------------- /policies/example_test.rego: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | test_violation_kind_fail { 4 | input := { 5 | "kind": "Invalid", 6 | "version": { "major": 1, "minor": 0, "patch": 0 }, 7 | "target": { 8 | "artefact": { 9 | "source": { "type": "maven2" }, 10 | "group": "a", 11 | "name": "b", 12 | "version": "c" 13 | } 14 | } 15 | } 16 | 17 | result := violations with input as input 18 | count(result) = 1 19 | } 20 | 21 | test_log4j_old_version_fail { 22 | input := { 23 | "kind": "PolicyInput", 24 | "version": { "major": 1, "minor": 0, "patch": 0 }, 25 | "target": { 26 | "artefact": { 27 | "source": { "type": "maven2" }, 28 | "group": "org.apache.logging.log4j", 29 | "name": "log4j", 30 | "version": "2.16.0" 31 | } 32 | } 33 | } 34 | 35 | result := violations with input as input 36 | count(result) = 1 37 | } 38 | 39 | test_log4j_old_version_pass { 40 | input := { 41 | "kind": "PolicyInput", 42 | "version": { "major": 1, "minor": 0, "patch": 0 }, 43 | "target": { 44 | "artefact": { 45 | "source": { "type": "maven2" }, 46 | "group": "org.apache.logging.log4j", 47 | "name": "log4j", 48 | "version": "2.17.2" 49 | } 50 | } 51 | } 52 | 53 | result := violations with input as input 54 | count(result) = 0 55 | } 56 | 57 | test_fail_on_critical_vuln { 58 | input := { 59 | "kind": "PolicyInput", 60 | "version": { "major": 1, "minor": 0, "patch": 0 }, 61 | "target": { 62 | "artefact": { 63 | "source": { "type": "maven2" }, 64 | "group": "org.example", 65 | "name": "random", 66 | "version": "1.33.7", 67 | }, 68 | "vulnerabilities": [ 69 | { "severity": "CRITICAL" } 70 | ] 71 | } 72 | } 73 | 74 | result := violations with input as input 75 | count(result) = 1 76 | } 77 | 78 | test_pass_on_low_vuln { 79 | input := { 80 | "kind": "PolicyInput", 81 | "version": { "major": 1, "minor": 0, "patch": 0 }, 82 | "target": { 83 | "artefact": { 84 | "source": { "type": "maven2" }, 85 | "group": "org.example", 86 | "name": "random", 87 | "version": "1.33.7", 88 | }, 89 | "vulnerabilities": [ 90 | { "severity": "LOW" } 91 | ] 92 | } 93 | } 94 | 95 | result := violations with input as input 96 | count(result) = 0 97 | } 98 | 99 | test_fail_private_namespace { 100 | input := { 101 | "kind": "PolicyInput", 102 | "version": { "major": 1, "minor": 0, "patch": 0 }, 103 | "target": { 104 | "artefact": { 105 | "source": { "type": "maven2" }, 106 | "group": "org.example.private.lib", 107 | "name": "random", 108 | "version": "1.33.7", 109 | }, 110 | "vulnerabilities": [ 111 | { "severity": "LOW" } 112 | ] 113 | } 114 | } 115 | 116 | result := violations with input as input 117 | count(result) = 1 118 | } 119 | -------------------------------------------------------------------------------- /services/.dockerignore: -------------------------------------------------------------------------------- 1 | out 2 | gen 3 | -------------------------------------------------------------------------------- /services/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | pkg/common/openssf/osv.types.go 3 | gen 4 | 5 | -------------------------------------------------------------------------------- /services/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Current Service", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${file}", 13 | "env": { 14 | "GLOBAL_CONFIG_PATH": "${workspaceFolder}/../config/global.yml", 15 | "SERVICE_TLS_CERT": "${workspaceFolder}/../pki/tap/server.crt", 16 | "SERVICE_TLS_KEY": "${workspaceFolder}/../pki/tap/server.key", 17 | "SERVICE_TLS_ROOT_CA": "${workspaceFolder}/../pki/root.crt" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /services/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-buster AS build 2 | 3 | RUN apt-get update && apt-get install -y protobuf-compiler 4 | 5 | WORKDIR /build 6 | 7 | COPY go.mod go.sum Makefile ./ 8 | 9 | RUN go mod download && mkdir gen 10 | RUN make oapi-codegen-install protoc-install 11 | 12 | COPY . . 13 | 14 | ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64 15 | RUN make 16 | 17 | FROM gcr.io/distroless/base-debian10 18 | 19 | WORKDIR /app 20 | 21 | COPY --from=build /build/out /app/server 22 | 23 | ENV PATH "${PATH}:/app/server" 24 | EXPOSE 9000 9001 9002 25 | 26 | USER nonroot:nonroot 27 | -------------------------------------------------------------------------------- /services/Makefile: -------------------------------------------------------------------------------- 1 | all: clean setup server 2 | 3 | oapi-codegen-install: 4 | go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.10.1 5 | 6 | protoc-install: 7 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 8 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 9 | go install github.com/envoyproxy/protoc-gen-validate@latest 10 | 11 | oapi-codegen: 12 | oapi-codegen -package openssf -generate types ./spec/openssf/osv-api-openapi.yml > ./pkg/common/openssf/osv.types.go 13 | 14 | protoc-codegen: 15 | protoc -I ./spec/proto/ \ 16 | -I ./spec/proto/lib/ -I ./spec/proto/lib/protoc-gen-validate \ 17 | --go_out=./gen/ \ 18 | --go-grpc_out=./gen/ \ 19 | --validate_out="lang=go:./gen/" \ 20 | --go_opt=paths=source_relative \ 21 | --go-grpc_opt=paths=source_relative \ 22 | --validate_opt=paths=source_relative \ 23 | ./spec/proto/pds.proto 24 | 25 | protoc -I ./spec/proto/ \ 26 | -I ./spec/proto/lib/ -I ./spec/proto/lib/protoc-gen-validate \ 27 | --go_out=./gen/ \ 28 | --go-grpc_out=./gen/ \ 29 | --validate_out="lang=go:./gen/" \ 30 | --go_opt=paths=source_relative \ 31 | --go-grpc_opt=paths=source_relative \ 32 | --validate_opt=paths=source_relative \ 33 | ./spec/proto/raya.proto 34 | 35 | protoc -I ./spec/proto/ \ 36 | -I ./spec/proto/lib/ -I ./spec/proto/lib/protoc-gen-validate \ 37 | --go_out=./gen/ \ 38 | --validate_out="lang=go:./gen/" \ 39 | --go_opt=paths=source_relative \ 40 | --validate_opt=paths=source_relative \ 41 | ./spec/proto/models.proto 42 | 43 | protoc -I ./spec/proto/ \ 44 | -I ./spec/proto/lib/ -I ./spec/proto/lib/protoc-gen-validate \ 45 | --go_out=./gen/ \ 46 | --validate_out="lang=go:./gen/" \ 47 | --go_opt=paths=source_relative \ 48 | --validate_opt=paths=source_relative \ 49 | ./spec/proto/events.proto 50 | 51 | protoc -I ./spec/proto/ \ 52 | -I ./spec/proto/lib/ -I ./spec/proto/lib/protoc-gen-validate \ 53 | --go_out=./gen/ \ 54 | --validate_out="lang=go:./gen/" \ 55 | --go_opt=paths=source_relative \ 56 | --validate_opt=paths=source_relative \ 57 | ./spec/proto/config.proto 58 | 59 | 60 | setup: 61 | mkdir -p out 62 | 63 | server: oapi-codegen protoc-codegen 64 | go build -o out/pdp-server cmd/pdp/pdp.go 65 | go build -o out/tap-server cmd/tap/tap.go 66 | go build -o out/dcs-server cmd/dcs/dcs.go 67 | go build -o out/pds-server cmd/pds/pds.go 68 | 69 | .PHONY: test 70 | test: 71 | go test ./... 72 | 73 | .PHONY: clean 74 | clean: 75 | -rm -rf out 76 | 77 | gosec: 78 | -docker run --rm -it -w /app/ -v `pwd`:/app/ securego/gosec \ 79 | -exclude-dir=/app/gen -exclude-dir=/app/spec \ 80 | /app/... 81 | -------------------------------------------------------------------------------- /services/cmd/confli/README.md: -------------------------------------------------------------------------------- 1 | # Configuration Command Line Interface 2 | 3 | A cli tool for validating and editing configuration 4 | -------------------------------------------------------------------------------- /services/cmd/confli/confli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 11 | ) 12 | 13 | var ( 14 | fileRepoPath string 15 | command string 16 | gatewayName string 17 | gatewayDomain string 18 | natsUrl string 19 | ) 20 | 21 | const ( 22 | commandValidateConf = "validate" 23 | commandGenerateSampleConf = "generate-sample" 24 | commandGenerateEnvoyConf = "generate-envoy" 25 | ) 26 | 27 | type commandHandler func() error 28 | 29 | var ( 30 | commandsTable = map[string]commandHandler{ 31 | commandValidateConf: func() error { 32 | return validateConfigCommand() 33 | }, 34 | commandGenerateSampleConf: func() error { 35 | return generateSampleConfCommand() 36 | }, 37 | commandGenerateEnvoyConf: func() error { 38 | return generateEnvoyConfigCommand() 39 | }, 40 | } 41 | ) 42 | 43 | func init() { 44 | flag.StringVar(&fileRepoPath, "file", "", "YAML file path for configuration") 45 | flag.StringVar(&command, "command", commandValidateConf, "Command to invoke") 46 | flag.StringVar(&gatewayName, "gateway-name", "localhost", "Command to invoke") 47 | flag.StringVar(&gatewayDomain, "gateway-domain", "localhost", "Command to invoke") 48 | flag.StringVar(&natsUrl, "nats-url", "tls://nats-server:4222", "NATS URL for messaging") 49 | 50 | flag.Usage = func() { 51 | fmt.Fprintf(os.Stderr, "%s Usage:\n", os.Args[0]) 52 | flag.PrintDefaults() 53 | 54 | fmt.Fprintf(os.Stderr, "\nAvailable commands:\n") 55 | for commandName, _ := range commandsTable { 56 | fmt.Fprintf(os.Stderr, "\t%s\n", commandName) 57 | } 58 | } 59 | } 60 | 61 | func main() { 62 | flag.Parse() 63 | logger.Init("confli") 64 | 65 | ch := commandsTable[command] 66 | if ch == nil { 67 | logger.Fatalf("Unknown command: %s", command) 68 | } 69 | 70 | err := ch() 71 | if err != nil { 72 | logger.Errorf("Command exec returned error: %v", err) 73 | } 74 | } 75 | 76 | func validateConfigCommand() error { 77 | if utils.IsEmptyString(fileRepoPath) { 78 | flag.Usage() 79 | os.Exit(-1) 80 | } 81 | 82 | _, err := config.NewConfigFileRepository(fileRepoPath, false, false) 83 | if err != nil { 84 | logger.Fatalf("Failed to create config repo: %v", err) 85 | } 86 | 87 | logger.Infof("Config file loaded and validated from: %s", fileRepoPath) 88 | return nil 89 | } 90 | 91 | func generateSampleConfCommand() error { 92 | return newSampleConfigGenerator(fileRepoPath).generate() 93 | } 94 | 95 | func generateEnvoyConfigCommand() error { 96 | return newEnvoyConfigGenerator(fileRepoPath).generate() 97 | } 98 | -------------------------------------------------------------------------------- /services/cmd/confli/sample_generator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 7 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 9 | "github.com/golang/protobuf/jsonpb" 10 | ) 11 | 12 | const ( 13 | ingressGatewayBasicAuthenticatorName = "default-basic-auth" 14 | ingressGatewayBasicAuthCredentialsFile = "/auth/basic-auth-credentials.txt" 15 | messagingAdapterNameNATS = "nats" 16 | messagingAdapterNameKafka = "kafka" 17 | ) 18 | 19 | type sampleConfigGenerator struct { 20 | file string 21 | } 22 | 23 | func newSampleConfigGenerator(path string) *sampleConfigGenerator { 24 | return &sampleConfigGenerator{file: path} 25 | } 26 | 27 | func (s *sampleConfigGenerator) generate() error { 28 | gateway := &config_api.GatewayConfiguration{} 29 | 30 | s.addInfo(gateway) 31 | s.addListener(gateway) 32 | s.addDefaultUpstreams(gateway) 33 | s.addDefaultGatewayAuth(gateway) 34 | s.addMessaging(gateway) 35 | s.addServiceConfig(gateway) 36 | 37 | s.printConfig(gateway) 38 | 39 | return nil 40 | } 41 | 42 | // We serialize to JSON first because proto generated classes has JSON 43 | // key name annotations 44 | func (s *sampleConfigGenerator) printConfig(gateway *config_api.GatewayConfiguration) { 45 | m := jsonpb.Marshaler{Indent: " "} 46 | data, err := m.MarshalToString(gateway) 47 | if err != nil { 48 | logger.Errorf("Failed to JSON serialize gateway config: %v", err) 49 | return 50 | } 51 | 52 | os.Stdout.Write([]byte(data)) 53 | } 54 | 55 | func (s *sampleConfigGenerator) addListener(gateway *config_api.GatewayConfiguration) { 56 | gateway.Listener = &config_api.GatewayConfiguration_Listener{ 57 | Host: "0.0.0.0", 58 | Port: 10000, 59 | } 60 | } 61 | 62 | func (s *sampleConfigGenerator) addInfo(gateway *config_api.GatewayConfiguration) { 63 | gateway.Info = &config_api.GatewayInfo{ 64 | Id: utils.NewUniqueId(), 65 | Name: gatewayName, 66 | Domain: gatewayDomain, 67 | } 68 | } 69 | 70 | func (s *sampleConfigGenerator) addDefaultGatewayAuth(gateway *config_api.GatewayConfiguration) { 71 | gateway.Authenticators = map[string]*config_api.GatewayAuthenticator{ 72 | ingressGatewayBasicAuthenticatorName: { 73 | Type: config_api.GatewayAuthenticationType_Basic, 74 | Config: &config_api.GatewayAuthenticator_BasicAuth{ 75 | BasicAuth: &config_api.GatewayAuthenticatorBasicAuth{ 76 | Path: ingressGatewayBasicAuthCredentialsFile, 77 | }, 78 | }, 79 | }, 80 | } 81 | } 82 | 83 | func (s *sampleConfigGenerator) addDefaultUpstreams(gateway *config_api.GatewayConfiguration) { 84 | gateway.Upstreams = []*config_api.GatewayUpstream{} 85 | 86 | gateway.Upstreams = append(gateway.Upstreams, 87 | s.getUpstream("maven-central", config_api.GatewayUpstreamType_Maven, 88 | config_api.GatewayUpstreamManagementType_GatewayAdmin, "/maven2", "/maven2", 89 | "repo.maven.apache.org", "443")) 90 | 91 | gateway.Upstreams = append(gateway.Upstreams, s.getUpstream("gradle-plugins", config_api.GatewayUpstreamType_Maven, 92 | config_api.GatewayUpstreamManagementType_GatewayAdmin, "/gradle-plugins/m2", "/m2", 93 | "plugins.gradle.org", "443")) 94 | 95 | gateway.Upstreams = append(gateway.Upstreams, s.getUpstream("pypi_org", config_api.GatewayUpstreamType_PyPI, 96 | config_api.GatewayUpstreamManagementType_GatewayAdmin, "/pypi", "/pypi", "pypi.org", "443")) 97 | } 98 | 99 | func (s *sampleConfigGenerator) addMessaging(gateway *config_api.GatewayConfiguration) { 100 | gateway.Messaging = map[string]*config_api.MessagingAdapter{ 101 | messagingAdapterNameNATS: { 102 | Type: config_api.MessagingAdapter_NATS, 103 | Config: &config_api.MessagingAdapter_Nats{ 104 | Nats: &config_api.MessagingAdapter_NatsAdapterConfig{ 105 | Url: natsUrl, 106 | }, 107 | }, 108 | }, 109 | messagingAdapterNameKafka: { 110 | Type: config_api.MessagingAdapter_KAFKA, 111 | Config: &config_api.MessagingAdapter_Kafka{ 112 | Kafka: &config_api.MessagingAdapter_KafkaAdapterConfig{ 113 | BootstrapServers: []string{"kafka-host:9092"}, 114 | SchemaRegistryUrl: "http://kafka-host:8081", 115 | }, 116 | }, 117 | }, 118 | } 119 | } 120 | 121 | func (s *sampleConfigGenerator) addServiceConfig(gateway *config_api.GatewayConfiguration) { 122 | gateway.Services = &config_api.GatewayConfiguration_ServiceConfig{} 123 | 124 | s.addPdpServiceConfig(gateway.Services) 125 | s.addTapServiceConfig(gateway.Services) 126 | s.addDcsServiceConfig(gateway.Services) 127 | } 128 | 129 | func (s *sampleConfigGenerator) addPdpServiceConfig(config *config_api.GatewayConfiguration_ServiceConfig) { 130 | config.Pdp = &config_api.PdpServiceConfig{ 131 | MonitorMode: true, 132 | PublisherConfig: &config_api.PdpServiceConfig_PublisherConfig{ 133 | MessagingAdapterName: messagingAdapterNameNATS, 134 | TopicNames: &config_api.PdpServiceConfig_PublisherConfig_TopicNames{ 135 | PolicyAudit: "gateway.pdp.audits", 136 | }, 137 | }, 138 | PdsClient: &config_api.PdsClientConfig{ 139 | Type: config_api.PdsClientType_LOCAL, 140 | Config: &config_api.PdsClientConfig_Common{ 141 | Common: &config_api.PdsClientCommonConfig{ 142 | Host: "pds", 143 | Port: 9002, 144 | Mtls: true, 145 | }, 146 | }, 147 | }, 148 | } 149 | } 150 | 151 | func (s *sampleConfigGenerator) addTapServiceConfig(config *config_api.GatewayConfiguration_ServiceConfig) { 152 | config.Tap = &config_api.TapServiceConfig{ 153 | PublisherConfig: &config_api.TapServiceConfig_PublisherConfig{ 154 | MessagingAdapterName: messagingAdapterNameNATS, 155 | TopicNames: &config_api.TapServiceConfig_PublisherConfig_TopicNames{ 156 | UpstreamRequest: "gateway.tap.upstream_req", 157 | UpstreamResponse: "gateway.tap.upstream_res", 158 | }, 159 | }, 160 | } 161 | } 162 | 163 | func (s *sampleConfigGenerator) addDcsServiceConfig(config *config_api.GatewayConfiguration_ServiceConfig) { 164 | config.Dcs = &config_api.DcsServiceConfig{ 165 | Active: true, 166 | MessagingAdapterName: messagingAdapterNameNATS, 167 | } 168 | } 169 | 170 | func (s *sampleConfigGenerator) getUpstream(name string, 171 | uType config_api.GatewayUpstreamType, mType config_api.GatewayUpstreamManagementType, 172 | pathPrefix string, pathRewrite string, host string, port string) *config_api.GatewayUpstream { 173 | 174 | return &config_api.GatewayUpstream{ 175 | Name: name, 176 | Type: uType, 177 | ManagementType: mType, 178 | Authentication: &config_api.GatewayAuthenticationProvider{ 179 | Type: config_api.GatewayAuthenticationType_Basic, 180 | Provider: ingressGatewayBasicAuthenticatorName, 181 | }, 182 | Route: &config_api.GatewayUpstreamRoute{ 183 | PathPrefix: pathPrefix, 184 | HostRewriteValue: host, 185 | PathPrefixRewriteValue: pathRewrite, 186 | }, 187 | Repository: &config_api.GatewayUpstreamRepository{ 188 | Host: host, 189 | Port: port, 190 | Tls: true, 191 | Sni: host, 192 | Authentication: &config_api.GatewayAuthenticationProvider{}, 193 | }, 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /services/cmd/dcs/dcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db" 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db/adapters" 11 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 12 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/messaging" 13 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/obs" 14 | "github.com/abhisek/supply-chain-gateway/services/pkg/dcs" 15 | ) 16 | 17 | func main() { 18 | logger.Init("dcs") 19 | config.Bootstrap("", true) 20 | 21 | tracerShutDown := obs.InitTracing() 22 | defer tracerShutDown(context.Background()) 23 | 24 | msgAdapter, err := config. 25 | GetMessagingConfigByName(config. 26 | DcsServiceConfig().GetMessagingAdapterName()) 27 | if err != nil { 28 | logger.Fatalf("Failed to get messaging adapter config") 29 | } 30 | 31 | msgService, err := messaging.NewService(msgAdapter) 32 | if err != nil { 33 | logger.Fatalf("Failed to create messaging service: %v", err) 34 | } 35 | 36 | mysqlPort, err := strconv.ParseInt(os.Getenv("MYSQL_SERVER_PORT"), 0, 16) 37 | if err != nil { 38 | logger.Fatalf("Failed to parse mysql server port: %v", err) 39 | } 40 | 41 | mysqlAdapter, err := adapters.NewMySqlAdapter(adapters.MySqlAdapterConfig{ 42 | Host: os.Getenv("MYSQL_SERVER_HOST"), 43 | Port: int16(mysqlPort), 44 | Username: os.Getenv("MYSQL_USER"), 45 | Password: os.Getenv("MYSQL_PASSWORD"), 46 | Database: os.Getenv("MYSQL_DATABASE"), 47 | }) 48 | if err != nil { 49 | logger.Fatalf("Failed to initialize MySQL adapter: %v", err) 50 | } 51 | 52 | err = db.MigrateSqlModels(mysqlAdapter) 53 | if err != nil { 54 | logger.Fatalf("Failed to run MySQL migration: %v", err) 55 | } 56 | 57 | repository, err := db.NewVulnerabilityRepository(mysqlAdapter) 58 | if err != nil { 59 | logger.Fatalf("Failed to create vulnerability repository") 60 | } 61 | 62 | dcs, err := dcs.NewDataCollectionService(msgService, repository) 63 | if err != nil { 64 | logger.Fatalf("Failed to created DCS: %v", err) 65 | } 66 | 67 | logger.Infof("Starting data collector service(s)") 68 | dcs.Start() 69 | } 70 | -------------------------------------------------------------------------------- /services/cmd/pdp/pdp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | common_adapters "github.com/abhisek/supply-chain-gateway/services/pkg/common/adapters" 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 11 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/messaging" 12 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/obs" 13 | 14 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 15 | "github.com/abhisek/supply-chain-gateway/services/pkg/pdp" 16 | envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" 17 | "google.golang.org/grpc" 18 | ) 19 | 20 | func main() { 21 | logger.Init("pdp") 22 | config.Bootstrap("", true) 23 | 24 | tracerShutDown := obs.InitTracing() 25 | defer tracerShutDown(context.Background()) 26 | 27 | policyDataService, err := pdp.NewPolicyDataServiceClient(config.PdpServiceConfig().GetPdsClient()) 28 | if err != nil { 29 | logger.Fatalf("Failed to create policy data service client: %v", err) 30 | } 31 | 32 | messagingService, err := buildMessagingService() 33 | if err != nil { 34 | logger.Fatalf("Failed to build messaging service: %v", err) 35 | } 36 | 37 | authService, err := pdp.NewAuthorizationService(policyDataService, messagingService) 38 | if err != nil { 39 | logger.Fatalf("Failed to create auth service: %s", err.Error()) 40 | } 41 | 42 | common_adapters.StartGrpcServer("PDP", "0.0.0.0", "9000", 43 | []grpc.ServerOption{}, func(s *grpc.Server) { 44 | envoy_service_auth_v3.RegisterAuthorizationServer(s, authService) 45 | }) 46 | } 47 | 48 | func buildMessagingService() (messaging.MessagingService, error) { 49 | cfg := config.PdpServiceConfig() 50 | messageAdapter, err := config.GetMessagingConfigByName(cfg.PublisherConfig.MessagingAdapterName) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | // FIXME: Migrate to messaging.NewService(...) config based factory 56 | switch messageAdapter.Type { 57 | case config_api.MessagingAdapter_KAFKA: 58 | logger.Infof("Using Kafka (pongo) messaging service") 59 | return messaging.NewKafkaProtobufMessagingService(os.Getenv("PDP_KAFKA_PONGO_BOOTSTRAP_SERVERS"), 60 | os.Getenv("PDP_KAFKA_PONGO_SCHEMA_REGISTRY_URL")) 61 | case config_api.MessagingAdapter_NATS: 62 | logger.Infof("Using NATs messaging service") 63 | return messaging.NewNatsMessagingService(messageAdapter) 64 | default: 65 | return nil, fmt.Errorf("unknown message adapter type: %s", messageAdapter.Type.String()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /services/cmd/pds/pds.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "strconv" 8 | 9 | api "github.com/abhisek/supply-chain-gateway/services/gen" 10 | 11 | common_adapters "github.com/abhisek/supply-chain-gateway/services/pkg/common/adapters" 12 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 13 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 14 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/obs" 15 | "google.golang.org/grpc" 16 | 17 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db" 18 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db/adapters" 19 | "github.com/abhisek/supply-chain-gateway/services/pkg/pds" 20 | ) 21 | 22 | func main() { 23 | logger.Init("dcs") 24 | config.Bootstrap("", true) 25 | 26 | tracerShutDown := obs.InitTracing() 27 | defer tracerShutDown(context.Background()) 28 | 29 | mysqlPort, err := strconv.ParseInt(os.Getenv("MYSQL_SERVER_PORT"), 0, 16) 30 | if err != nil { 31 | log.Fatalf("Failed to parse mysql server port: %v", err) 32 | } 33 | 34 | mysqlAdapter, err := adapters.NewMySqlAdapter(adapters.MySqlAdapterConfig{ 35 | Host: os.Getenv("MYSQL_SERVER_HOST"), 36 | Port: int16(mysqlPort), 37 | Username: os.Getenv("MYSQL_USER"), 38 | Password: os.Getenv("MYSQL_PASSWORD"), 39 | Database: os.Getenv("MYSQL_DATABASE"), 40 | }) 41 | 42 | if err != nil { 43 | log.Fatalf("Failed to initialize MySQL adapter: %v", err) 44 | } 45 | 46 | repository, err := db.NewVulnerabilityRepository(mysqlAdapter) 47 | if err != nil { 48 | log.Fatalf("Failed to create vulnerability repository") 49 | } 50 | 51 | pdService, err := pds.NewPolicyDataService(repository) 52 | if err != nil { 53 | log.Fatalf("Failed to create policy data service") 54 | } 55 | 56 | common_adapters.StartGrpcMtlsServer("PDS", os.Getenv("PDS_SERVER_NAME"), "0.0.0.0", "9002", 57 | []grpc.ServerOption{grpc.MaxConcurrentStreams(5000)}, func(s *grpc.Server) { 58 | api.RegisterPolicyDataServiceServer(s, pdService) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /services/cmd/tap/tap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | common_adapters "github.com/abhisek/supply-chain-gateway/services/pkg/common/adapters" 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/messaging" 11 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/obs" 12 | "github.com/abhisek/supply-chain-gateway/services/pkg/tap" 13 | 14 | envoy_v3_ext_proc_pb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" 15 | 16 | "google.golang.org/grpc" 17 | ) 18 | 19 | func main() { 20 | logger.Init("tap") 21 | config.Bootstrap("", true) 22 | 23 | tracerShutDown := obs.InitTracing() 24 | defer tracerShutDown(context.Background()) 25 | 26 | messageAdapter, err := config.GetMessagingConfigByName(config. 27 | TapServiceConfig().PublisherConfig.MessagingAdapterName) 28 | if err != nil { 29 | logger.Fatalf("failed to get messaging config: %v", err) 30 | } 31 | 32 | msgService, err := messaging.NewNatsMessagingService(messageAdapter) 33 | if err != nil { 34 | log.Fatalf("Failed to create messaging service: %v", err) 35 | } 36 | 37 | tapService, err := tap.NewTapService(msgService, []tap.TapHandlerRegistration{ 38 | tap.NewTapEventPublisherRegistration(msgService), 39 | }) 40 | 41 | if err != nil { 42 | log.Fatalf("Failed to create tap service: %s", err.Error()) 43 | } 44 | 45 | common_adapters.StartGrpcServer("TAP", "0.0.0.0", "9001", 46 | []grpc.ServerOption{grpc.MaxConcurrentStreams(5000)}, func(s *grpc.Server) { 47 | envoy_v3_ext_proc_pb.RegisterExternalProcessorServer(s, tapService) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /services/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abhisek/supply-chain-gateway/services 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/confluentinc/confluent-kafka-go v1.9.1 7 | github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 8 | github.com/envoyproxy/protoc-gen-validate v0.1.0 9 | github.com/gojek/heimdall/v7 v7.0.2 10 | github.com/golang/protobuf v1.5.2 11 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 12 | github.com/mitchellh/mapstructure v1.1.2 13 | github.com/nats-io/nats.go v1.14.0 14 | github.com/oklog/ulid/v2 v2.0.2 15 | github.com/open-policy-agent/opa v0.39.0 16 | github.com/stretchr/testify v1.8.0 17 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0 18 | go.opentelemetry.io/otel v1.9.0 19 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.6.1 20 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.6.1 21 | go.opentelemetry.io/otel/sdk v1.6.1 22 | go.opentelemetry.io/otel/trace v1.9.0 23 | go.uber.org/zap v1.23.0 24 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 25 | golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2 26 | google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29 27 | google.golang.org/grpc v1.48.0 28 | google.golang.org/protobuf v1.28.0 29 | gorm.io/datatypes v1.0.6 30 | gorm.io/driver/mysql v1.3.2 31 | gorm.io/gorm v1.23.5 32 | ) 33 | 34 | require ( 35 | github.com/DataDog/datadog-go v3.7.1+incompatible // indirect 36 | github.com/OneOfOne/xxhash v1.2.8 // indirect 37 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect 38 | github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c // indirect 39 | github.com/cenkalti/backoff/v4 v4.1.2 // indirect 40 | github.com/census-instrumentation/opencensus-proto v0.2.1 // indirect 41 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 // indirect 42 | github.com/davecgh/go-spew v1.1.1 // indirect 43 | github.com/ghodss/yaml v1.0.0 // indirect 44 | github.com/go-logr/logr v1.2.3 // indirect 45 | github.com/go-logr/stdr v1.2.2 // indirect 46 | github.com/go-sql-driver/mysql v1.6.0 // indirect 47 | github.com/gobwas/glob v0.2.3 // indirect 48 | github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect 49 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect 50 | github.com/jhump/protoreflect v1.12.0 // indirect 51 | github.com/jinzhu/inflection v1.0.0 // indirect 52 | github.com/jinzhu/now v1.1.4 // indirect 53 | github.com/nats-io/nats-server/v2 v2.8.1 // indirect 54 | github.com/nats-io/nkeys v0.3.0 // indirect 55 | github.com/nats-io/nuid v1.0.1 // indirect 56 | github.com/pkg/errors v0.9.1 // indirect 57 | github.com/pmezard/go-difflib v1.0.0 // indirect 58 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 59 | github.com/stretchr/objx v0.4.0 // indirect 60 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 61 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 62 | github.com/yashtewari/glob-intersection v0.1.0 // indirect 63 | go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.6.1 // indirect 64 | go.opentelemetry.io/proto/otlp v0.12.1 // indirect 65 | go.uber.org/atomic v1.10.0 // indirect 66 | go.uber.org/multierr v1.8.0 // indirect 67 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect 68 | golang.org/x/text v0.3.7 // indirect 69 | golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect 70 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 71 | gopkg.in/yaml.v2 v2.4.0 // indirect 72 | gopkg.in/yaml.v3 v3.0.1 // indirect 73 | ) 74 | -------------------------------------------------------------------------------- /services/pkg/auth/auth_credential.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "strings" 4 | 5 | // Represents an user supplied credential 6 | type authCredential struct { 7 | userId string 8 | userSecret string 9 | } 10 | 11 | // Handle the form `project-id/user@org` ignoring the `project-id` 12 | // while returning the userId 13 | func (a *authCredential) UserId() string { 14 | userId := a.userId 15 | if strings.Index(userId, "/") >= 0 { 16 | userId = strings.SplitN(userId, "/", 2)[1] 17 | } 18 | 19 | return userId 20 | } 21 | 22 | func (a *authCredential) ProjectId() string { 23 | if strings.Index(a.userId, "/") >= 0 { 24 | return strings.SplitN(a.userId, "/", 2)[0] 25 | } 26 | 27 | return "" 28 | } 29 | 30 | func (a *authCredential) OrgId() string { 31 | uId := a.UserId() 32 | if strings.Index(uId, "@") >= 0 { 33 | return strings.SplitN(uId, "@", 2)[1] 34 | } 35 | 36 | return "" 37 | } 38 | 39 | func (a *authCredential) UserSecret() string { 40 | return a.userSecret 41 | } 42 | -------------------------------------------------------------------------------- /services/pkg/auth/auth_credential_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestAuthCredential(t *testing.T) { 10 | cases := []struct { 11 | name string 12 | inputUserId string 13 | inputUserSecret string 14 | 15 | outputUserId string 16 | outputOrgId string 17 | outputProjectId string 18 | outputUserSecret string 19 | }{ 20 | { 21 | "Full userId format", 22 | "projectId/username@orgName", 23 | "userSecret", 24 | 25 | "username@orgName", 26 | "orgName", 27 | "projectId", 28 | "userSecret", 29 | }, 30 | { 31 | "ProjectId is not given", 32 | "username@orgName", 33 | "userSecret", 34 | 35 | "username@orgName", 36 | "orgName", 37 | "", 38 | "userSecret", 39 | }, 40 | { 41 | "ProjectId and OrgId is not given", 42 | "username", 43 | "userSecret", 44 | 45 | "username", 46 | "", 47 | "", 48 | "userSecret", 49 | }, 50 | { 51 | "Starts with a Slash", 52 | "/projectId/username@orgName", 53 | "userSecret", 54 | 55 | "projectId/username@orgName", 56 | "orgName", 57 | "", 58 | "userSecret", 59 | }, 60 | { 61 | "Double Slash in UserId", 62 | "projectId//username@orgName", 63 | "userSecret", 64 | 65 | "/username@orgName", 66 | "orgName", 67 | "projectId", 68 | "userSecret", 69 | }, 70 | { 71 | "Double @ in UserId", 72 | "projectId/username@@orgName", 73 | "userSecret", 74 | 75 | "username@@orgName", 76 | "@orgName", 77 | "projectId", 78 | "userSecret", 79 | }, 80 | { 81 | "Username ending with Slash", 82 | "projectId/username/@orgName", 83 | "userSecret", 84 | 85 | "username/@orgName", 86 | "orgName", 87 | "projectId", 88 | "userSecret", 89 | }, 90 | { 91 | "User Secret has special chars", 92 | "projectId/username@orgName", 93 | "@@///@@/@@", 94 | 95 | "username@orgName", 96 | "orgName", 97 | "projectId", 98 | "@@///@@/@@", 99 | }, 100 | } 101 | 102 | for _, test := range cases { 103 | t.Run(test.name, func(t *testing.T) { 104 | creds := authCredential{userId: test.inputUserId, userSecret: test.inputUserSecret} 105 | 106 | assert.Equal(t, creds.UserId(), test.outputUserId) 107 | assert.Equal(t, creds.OrgId(), test.outputOrgId) 108 | assert.Equal(t, creds.ProjectId(), test.outputProjectId) 109 | assert.Equal(t, creds.UserSecret(), test.outputUserSecret) 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /services/pkg/auth/auth_identity.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | type authIdentity struct { 4 | idType, userId, orgId, projectId, name string 5 | } 6 | 7 | func (a *authIdentity) Type() string { 8 | return a.idType 9 | } 10 | 11 | func (a *authIdentity) UserId() string { 12 | return a.userId 13 | } 14 | 15 | func (a *authIdentity) Name() string { 16 | return a.name 17 | } 18 | 19 | func (a *authIdentity) OrgId() string { 20 | return a.orgId 21 | } 22 | 23 | func (a *authIdentity) ProjectId() string { 24 | return a.projectId 25 | } 26 | 27 | func AnonymousIdentity() AuthenticatedIdentity { 28 | return &authIdentity{idType: AuthIdentityTypeAnonymous, 29 | userId: "anonymous", name: "Anonymous User"} 30 | } 31 | -------------------------------------------------------------------------------- /services/pkg/auth/authentication.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | /** 4 | [Gateway] -> [Ingress Auth] -> PDP 5 | [Gateway] -> TAP -> [Egress Auth] -> Upstream Repo 6 | 7 | Risk? 8 | 9 | User tricking gateway to send credentials to malicious user controlled endpoint 10 | **/ 11 | 12 | import ( 13 | "context" 14 | 15 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 16 | ) 17 | 18 | const ( 19 | // PDP will lookup ingress authenticators 20 | AuthStageIngress = "ingress" // Gateway Auth 21 | 22 | // Tap will lookup egress authenticators 23 | AuthStageEgress = "egress" // Upstream Auth 24 | 25 | AuthIdentityTypeAnonymous = "Anonymous" 26 | AuthIdentityTypeBasicAuth = "BasicAuth" 27 | ) 28 | 29 | type AuthenticationProvider interface { 30 | IngressAuthService(common_models.ArtefactUpStream) (IngressAuthenticationService, error) 31 | EgressAuthService(common_models.ArtefactRepository) (EgressAuthenticationService, error) 32 | } 33 | 34 | // Adapter to wrap Envoy request to get credentials 35 | type AuthenticationCredentialProvider interface { 36 | Credential() (AuthenticationCredential, error) 37 | } 38 | 39 | // A provided or obtained credential for authentication 40 | type AuthenticationCredential interface { 41 | ProjectId() string 42 | OrgId() string 43 | UserId() string 44 | UserSecret() string 45 | } 46 | 47 | // Authenticated identity used in Ingress auth 48 | type AuthenticatedIdentity interface { 49 | Type() string 50 | OrgId() string 51 | ProjectId() string 52 | UserId() string 53 | Name() string 54 | } 55 | 56 | // Authentication for gateway users 57 | type IngressAuthenticationService interface { 58 | Authenticate(context.Context, AuthenticationCredentialProvider) (AuthenticatedIdentity, error) 59 | } 60 | 61 | // Apply credentials to outgoing request to repo 62 | type AuthenticationCredentialApplier interface { 63 | Apply(AuthenticationCredential) error 64 | } 65 | 66 | // Authenticate upstream repo request 67 | type EgressAuthenticationService interface { 68 | Authenticate(context.Context, AuthenticationCredentialApplier) error 69 | } 70 | -------------------------------------------------------------------------------- /services/pkg/auth/basic_auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 13 | "golang.org/x/crypto/bcrypt" 14 | ) 15 | 16 | var ( 17 | basicAuthUserNotFound = errors.New("user not found in basic auth db") 18 | basicAuthFailed = errors.New("authentication denied") 19 | basicAuthCredentialNotFound = errors.New("credential not found in request") 20 | basicAuthHashTypeUnsupported = errors.New("hash type is not supported") 21 | ) 22 | 23 | // Implement basic auth for gateway ingress 24 | type basicAuthProvider struct { 25 | file string 26 | credentials map[string]string 27 | } 28 | 29 | func NewIngressBasicAuthService(cfg *config_api.GatewayAuthenticatorBasicAuth) (IngressAuthenticationService, error) { 30 | p := &basicAuthProvider{file: cfg.Path} 31 | if err := p.loadCredentials(); err != nil { 32 | return nil, err 33 | } 34 | 35 | return p, nil 36 | } 37 | 38 | func (p *basicAuthProvider) Authenticate(ctx context.Context, cp AuthenticationCredentialProvider) (AuthenticatedIdentity, error) { 39 | creds, err := cp.Credential() 40 | if err != nil { 41 | return nil, basicAuthCredentialNotFound 42 | } 43 | 44 | hp, ok := p.credentials[creds.UserId()] 45 | if !ok { 46 | return nil, basicAuthUserNotFound 47 | } 48 | 49 | err = p.safeCompareHash(creds.UserSecret(), hp) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return &authIdentity{ 55 | idType: AuthIdentityTypeBasicAuth, 56 | userId: creds.UserId(), 57 | orgId: creds.OrgId(), 58 | projectId: creds.ProjectId(), 59 | name: fmt.Sprintf("Basic Auth User: %s", creds.UserId())}, nil 60 | } 61 | 62 | func (p *basicAuthProvider) loadCredentials() error { 63 | log.Printf("Loading basic auth credentials from: %s", p.file) 64 | 65 | file, err := os.OpenFile(p.file, os.O_RDONLY, os.ModePerm) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | defer file.Close() 71 | scanner := bufio.NewScanner(file) 72 | 73 | s := make(map[string]string, 0) 74 | for scanner.Scan() { 75 | parts := strings.SplitN(scanner.Text(), ":", 2) 76 | if len(parts) == 2 { 77 | s[parts[0]] = parts[1] 78 | } 79 | } 80 | 81 | if err := scanner.Err(); err != nil { 82 | return err 83 | } 84 | 85 | p.credentials = s 86 | return nil 87 | } 88 | 89 | func (p *basicAuthProvider) safeCompareHash(password string, hash string) error { 90 | if !strings.HasPrefix(hash, "$2y$") { 91 | return basicAuthHashTypeUnsupported 92 | } 93 | 94 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 95 | if err != nil { 96 | return basicAuthFailed 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /services/pkg/auth/envoy_adapter.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "strings" 7 | 8 | envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" 9 | ) 10 | 11 | type envoyIngressAuthAdapter struct { 12 | request *envoy_service_auth_v3.AttributeContext_HttpRequest 13 | } 14 | 15 | func NewEnvoyIngressAuthAdapter(req *envoy_service_auth_v3.AttributeContext_HttpRequest) AuthenticationCredentialProvider { 16 | return &envoyIngressAuthAdapter{request: req} 17 | } 18 | 19 | func (a *envoyIngressAuthAdapter) Credential() (AuthenticationCredential, error) { 20 | authHeader := a.request.Headers["authorization"] 21 | if authHeader == "" { 22 | return nil, errors.New("no authorization header found in request") 23 | } 24 | 25 | parts := strings.SplitN(authHeader, " ", 2) 26 | if len(parts) != 2 || !strings.EqualFold(parts[0], "basic") { 27 | return nil, errors.New("not a basic auth type") 28 | } 29 | 30 | decoded, err := base64.StdEncoding.DecodeString(parts[1]) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | pair := strings.SplitN(string(decoded), ":", 2) 36 | if len(pair) != 2 || pair[0] == "" { 37 | return nil, errors.New("invalid basic auth decoded pair") 38 | } 39 | 40 | return &authCredential{userId: pair[0], userSecret: pair[1]}, nil 41 | } 42 | -------------------------------------------------------------------------------- /services/pkg/auth/noauth-auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "context" 4 | 5 | type noAuthProvider struct{} 6 | 7 | func NewIngressNoAuthService() (IngressAuthenticationService, error) { 8 | return &noAuthProvider{}, nil 9 | } 10 | 11 | func (p *noAuthProvider) Authenticate(ctx context.Context, cp AuthenticationCredentialProvider) (AuthenticatedIdentity, error) { 12 | return AnonymousIdentity(), nil 13 | } 14 | -------------------------------------------------------------------------------- /services/pkg/auth/provider.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 9 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 10 | ) 11 | 12 | type authProvider struct { 13 | // Unbounded cache, should not be a problem because the 14 | // number of providers can be limited 15 | ingressCache map[string]IngressAuthenticationService 16 | egressCache map[string]EgressAuthenticationService 17 | } 18 | 19 | func NewAuthenticationProvider() AuthenticationProvider { 20 | return &authProvider{} 21 | } 22 | 23 | func (a *authProvider) IngressAuthService(upstream common_models.ArtefactUpStream) (IngressAuthenticationService, error) { 24 | cf := func(s func(c *config_api.GatewayAuthenticator) (IngressAuthenticationService, error)) (IngressAuthenticationService, error) { 25 | cfg, err := config.GetAuthenticatorByName(upstream.Authentication.Provider) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return s(cfg) 31 | } 32 | 33 | // TODO: Implement a cache for services to prevent reinitialize the same 34 | // authenticator, uniquely identified by a name 35 | 36 | if upstream.Authentication.IsBasic() { 37 | return cf(func(c *config_api.GatewayAuthenticator) (IngressAuthenticationService, error) { 38 | return NewIngressBasicAuthService(c.GetBasicAuth()) 39 | }) 40 | } else if upstream.Authentication.IsNoAuth() { 41 | return NewIngressNoAuthService() 42 | } else { 43 | return nil, fmt.Errorf("no auth service available for: %s", upstream.Authentication.Provider) 44 | } 45 | } 46 | 47 | func (a *authProvider) EgressAuthService(common_models.ArtefactRepository) (EgressAuthenticationService, error) { 48 | return nil, errors.New("unimplemented") 49 | } 50 | -------------------------------------------------------------------------------- /services/pkg/auth/utils.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | // Nothing :) 4 | -------------------------------------------------------------------------------- /services/pkg/common/adapters/grpc.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "time" 8 | 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/credentials" 11 | "google.golang.org/grpc/credentials/insecure" 12 | 13 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 14 | grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" 15 | grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator" 16 | 17 | "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 18 | grpcotel "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 19 | ) 20 | 21 | type GrpcAdapterConfigurer func(server *grpc.Server) 22 | type GrpcClientConfigurer func(conn *grpc.ClientConn) 23 | 24 | var ( 25 | NoGrpcDialOptions = []grpc.DialOption{} 26 | NoGrpcConfigurer = func(conn *grpc.ClientConn) {} 27 | ) 28 | 29 | func StartGrpcMtlsServer(name, serverName, host, port string, sopts []grpc.ServerOption, configure GrpcAdapterConfigurer) { 30 | tc, err := utils.TlsConfigFromEnvironment(serverName) 31 | if err != nil { 32 | log.Fatalf("Failed to setup TLS from environment: %v", err) 33 | } 34 | 35 | creds := credentials.NewTLS(&tc) 36 | sopts = append(sopts, grpc.Creds(creds)) 37 | 38 | StartGrpcServer(name, host, port, sopts, configure) 39 | } 40 | 41 | func StartGrpcServer(name, host, port string, sopts []grpc.ServerOption, configure GrpcAdapterConfigurer) { 42 | addr := net.JoinHostPort(host, port) 43 | listener, err := net.Listen("tcp", addr) 44 | 45 | if err != nil { 46 | log.Fatalf("Failed to listen on %s:%s - %s", host, port, err.Error()) 47 | } 48 | 49 | sopts = append(sopts, grpc.UnaryInterceptor( 50 | grpc_middleware.ChainUnaryServer( 51 | grpcotel.UnaryServerInterceptor(), 52 | grpc_validator.UnaryServerInterceptor(), 53 | ), 54 | )) 55 | 56 | sopts = append(sopts, grpc.StreamInterceptor( 57 | grpc_middleware.ChainStreamServer( 58 | grpcotel.StreamServerInterceptor(), 59 | grpc_validator.StreamServerInterceptor(), 60 | ), 61 | )) 62 | 63 | server := grpc.NewServer(sopts...) 64 | configure(server) 65 | 66 | log.Printf("Starting %s gRPC server on %s:%s", name, host, port) 67 | err = server.Serve(listener) 68 | 69 | log.Fatalf("gRPC Server exit: %s", err.Error()) 70 | } 71 | 72 | func GrpcMtlsClient(name, serverName, host, port string, dopts []grpc.DialOption, configurer GrpcClientConfigurer) (*grpc.ClientConn, error) { 73 | tc, err := grpcTransportCredentials(serverName) 74 | if err != nil { 75 | return nil, fmt.Errorf("failed to setup client transport credentials: %w", err) 76 | } 77 | 78 | dopts = append(dopts, tc) 79 | return grpcClient(name, host, port, dopts, configurer) 80 | } 81 | 82 | func GrpcInsecureClient(name, host, port string, dopts []grpc.DialOption, configurer GrpcClientConfigurer) (*grpc.ClientConn, error) { 83 | tc := grpc.WithTransportCredentials(insecure.NewCredentials()) 84 | dopts = append(dopts, tc) 85 | return grpcClient(name, host, port, dopts, configurer) 86 | } 87 | 88 | func grpcClient(name, host, port string, dopts []grpc.DialOption, configurer GrpcClientConfigurer) (*grpc.ClientConn, error) { 89 | log.Printf("[%s] Connecting to gRPC server %s:%s", name, host, port) 90 | 91 | dopts = append(dopts, grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor())) 92 | dopts = append(dopts, grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor())) 93 | 94 | retry := 5 95 | t := 1 96 | conn, err := grpc.Dial(net.JoinHostPort(host, port), dopts...) 97 | for err != nil && t < retry { 98 | log.Printf("[%d/%d] Retrying due to failure: %v", t, retry, err) 99 | conn, err = grpc.Dial(net.JoinHostPort(host, port), dopts...) 100 | 101 | time.Sleep(1 * time.Second) 102 | t += 1 103 | } 104 | 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | configurer(conn) 110 | return conn, nil 111 | } 112 | 113 | func grpcTransportCredentials(serverName string) (grpc.DialOption, error) { 114 | tlsConfig, err := utils.TlsConfigFromEnvironment(serverName) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | creds := credentials.NewTLS(&tlsConfig) 120 | return grpc.WithTransportCredentials(creds), nil 121 | } 122 | -------------------------------------------------------------------------------- /services/pkg/common/adapters/messaging.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | type MessagingHandlerFunc func(data []byte) error 4 | 5 | func StartMessagingListener(topic, group string, handler MessagingHandlerFunc) (<-chan bool, error) { 6 | waiter := make(chan bool) 7 | return waiter, nil 8 | } 9 | -------------------------------------------------------------------------------- /services/pkg/common/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | /** 4 | The config module provides access to currently available configuration. 5 | A repository is used as the source of configuration. 6 | 7 | Configuration at a high level is: 8 | 9 | 1. Bootstrap (can be dynamically updated) 10 | 2. Contextual (depends on current context data) 11 | */ 12 | 13 | import ( 14 | "fmt" 15 | "os" 16 | 17 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 18 | ) 19 | 20 | type configHolder struct { 21 | ctx any // TODO: Standardize domain context 22 | configRepository ConfigRepository 23 | } 24 | 25 | var ( 26 | globalConfig *configHolder = nil 27 | ) 28 | 29 | func Bootstrap(file string, reloadOnChange bool) { 30 | if globalConfig != nil { 31 | panic("config is already bootstrapped") 32 | } 33 | 34 | if file == "" { 35 | file = os.Getenv("GLOBAL_CONFIG_PATH") 36 | if file == "" { 37 | panic("no config source available") 38 | } 39 | } 40 | 41 | repository, err := NewConfigFileRepository(file, false, reloadOnChange) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | globalConfig = &configHolder{ 47 | configRepository: repository, 48 | } 49 | } 50 | 51 | // Get a contextual config 52 | func Contextual(ctx any) *configHolder { 53 | return current().withContext(ctx) 54 | } 55 | 56 | // Returns a wrapped configuration data with the wrapper 57 | // providing some convenient utility method 58 | func current() *configHolder { 59 | if globalConfig == nil { 60 | panic("config is used without bootstrap") 61 | } 62 | 63 | return globalConfig 64 | } 65 | 66 | func (cfg *configHolder) withContext(ctx any) *configHolder { 67 | return &configHolder{ 68 | configRepository: cfg.configRepository, 69 | ctx: ctx, 70 | } 71 | } 72 | 73 | // Returns the configuration data as per spec 74 | func g() *config_api.GatewayConfiguration { 75 | cfg, err := current().configRepository.LoadGatewayConfiguration() 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | return cfg 81 | } 82 | 83 | func GetMessagingConfigByName(name string) (*config_api.MessagingAdapter, error) { 84 | if mc, ok := g().Messaging[name]; ok { 85 | return mc, nil 86 | } else { 87 | return nil, fmt.Errorf("messaging adapter not found with name: %s", name) 88 | } 89 | } 90 | 91 | func GetAuthenticatorByName(name string) (*config_api.GatewayAuthenticator, error) { 92 | if a, ok := g().Authenticators[name]; ok { 93 | return a, nil 94 | } else { 95 | return nil, fmt.Errorf("authenticator not found with name: %s", name) 96 | } 97 | } 98 | 99 | func PdpServiceConfig() *config_api.PdpServiceConfig { 100 | return g().Services.GetPdp() 101 | } 102 | 103 | func DcsServiceConfig() *config_api.DcsServiceConfig { 104 | return g().Services.GetDcs() 105 | } 106 | 107 | func TapServiceConfig() *config_api.TapServiceConfig { 108 | return g().Services.GetTap() 109 | } 110 | 111 | func Upstreams() []*config_api.GatewayUpstream { 112 | return g().GetUpstreams() 113 | } 114 | 115 | func GetSecret(name string) (*config_api.GatewaySecret, error) { 116 | if s, ok := g().Secrets[name]; ok { 117 | return s, nil 118 | } else { 119 | return nil, fmt.Errorf("no secret found with name: %s", name) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /services/pkg/common/config/feature.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Implement poor man's feature flag based on some data source 11 | // For now, env + convention is a good data source 12 | 13 | const ( 14 | ffEnvPrefix = "FF" 15 | ) 16 | 17 | // Takes a string such as "app_dcs" and determine if 18 | // feature is explicitly disabled by looking up FF_APP_DCS_DISABLED=true 19 | func IsFeatureDisabled(key string) bool { 20 | key = fmt.Sprintf("%s_%s_disabled", ffEnvPrefix, key) 21 | key = strings.ToUpper(key) 22 | 23 | if v, b := os.LookupEnv(key); b { 24 | ret, err := strconv.ParseBool(v) 25 | if err == nil { 26 | return ret 27 | } 28 | } 29 | 30 | return false 31 | } 32 | -------------------------------------------------------------------------------- /services/pkg/common/config/repository.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 8 | ) 9 | 10 | const ( 11 | configRepositoryTypeFile = "file" 12 | ) 13 | 14 | // Define a repository interface to get the current configuration 15 | // the repository implementation can internally refresh / cache 16 | // configuration as required 17 | type ConfigRepository interface { 18 | LoadGatewayConfiguration() (*config_api.GatewayConfiguration, error) 19 | SaveGatewayConfiguration(config *config_api.GatewayConfiguration) error 20 | } 21 | 22 | func NewConfigRepository() (ConfigRepository, error) { 23 | cType := os.Getenv("BOOTSTRAP_CONFIGURATION_REPOSITORY_TYPE") 24 | switch cType { 25 | case configRepositoryTypeFile: 26 | return NewConfigFileRepository(os.Getenv("BOOTSTRAP_CONFIGURATION_REPOSITORY_PATH"), false, true) 27 | } 28 | 29 | return nil, fmt.Errorf("unknown config repository type: %s", cType) 30 | } 31 | -------------------------------------------------------------------------------- /services/pkg/common/config/repository_file.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 9 | "github.com/golang/protobuf/jsonpb" 10 | ) 11 | 12 | type configFileRepository struct { 13 | path string 14 | gatewayConfiguration *config_api.GatewayConfiguration 15 | m sync.RWMutex 16 | } 17 | 18 | func NewConfigFileRepository(path string, lazy bool, monitorForChange bool) (ConfigRepository, error) { 19 | r := &configFileRepository{path: path} 20 | var err error 21 | 22 | if !lazy { 23 | err = r.load() 24 | } 25 | 26 | if err == nil && monitorForChange { 27 | err = r.monitorForChange() 28 | } 29 | 30 | return r, err 31 | } 32 | 33 | func (c *configFileRepository) LoadGatewayConfiguration() (*config_api.GatewayConfiguration, error) { 34 | var err error = nil 35 | if c.gatewayConfiguration == nil { 36 | _ = c.load() 37 | } 38 | 39 | if c.gatewayConfiguration == nil { 40 | err = fmt.Errorf("gateway configuration is not loaded") 41 | } 42 | 43 | c.m.RLock() 44 | defer c.m.RUnlock() 45 | 46 | return c.gatewayConfiguration, err 47 | } 48 | 49 | func (c *configFileRepository) SaveGatewayConfiguration(config *config_api.GatewayConfiguration) error { 50 | return fmt.Errorf("persisting gateway configuration is not supported") 51 | } 52 | 53 | func (c *configFileRepository) load() error { 54 | file, err := os.Open(c.path) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | defer file.Close() 60 | 61 | var gatewayConfiguration config_api.GatewayConfiguration 62 | err = jsonpb.Unmarshal(file, &gatewayConfiguration) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | err = gatewayConfiguration.Validate() 68 | if err != nil { 69 | return err 70 | } 71 | 72 | c.m.Lock() 73 | defer c.m.Unlock() 74 | 75 | c.gatewayConfiguration = &gatewayConfiguration 76 | return nil 77 | } 78 | 79 | func (c *configFileRepository) monitorForChange() error { 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /services/pkg/common/config/translator.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/abhisek/supply-chain-gateway/services/gen" 4 | 5 | type PullTranslator[T any] interface { 6 | Translate(gen.GatewayConfiguration) (T, error) 7 | } 8 | 9 | type PushTranslator[T any] interface { 10 | RegisterReceiver(func(T, error) error) 11 | } 12 | -------------------------------------------------------------------------------- /services/pkg/common/db/adapters/mysql_adapter.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "golang.org/x/net/context" 9 | "gorm.io/driver/mysql" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type MySqlAdapter struct { 14 | db *gorm.DB 15 | config MySqlAdapterConfig 16 | } 17 | 18 | type MySqlAdapterConfig struct { 19 | Host string 20 | Port int16 21 | Username string 22 | Password string 23 | Database string 24 | } 25 | 26 | func NewMySqlAdapter(config MySqlAdapterConfig) (SqlDataAdapter, error) { 27 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", 28 | config.Username, config.Password, config.Host, config.Port, config.Database) 29 | 30 | log.Printf("Connecting to MySQL database with %s@%s:%d", config.Username, config.Host, config.Port) 31 | 32 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 33 | retry := 5 34 | t := 1 35 | 36 | // Retry connection to avoid race with DB container init 37 | for err != nil && t <= retry { 38 | log.Printf("[%d/%d] Failed to connect to MySQL server: %v", t, retry, err) 39 | db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) 40 | 41 | t += 1 42 | time.Sleep(1 * time.Second) 43 | } 44 | 45 | // Failed after retry 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | mysqlAdapter := &MySqlAdapter{db: db, config: config} 51 | err = mysqlAdapter.Ping() 52 | 53 | return mysqlAdapter, err 54 | } 55 | 56 | func (m *MySqlAdapter) GetDB() (*gorm.DB, error) { 57 | return m.db, nil 58 | } 59 | 60 | func (m *MySqlAdapter) Migrate(tables ...interface{}) error { 61 | return m.db.AutoMigrate(tables...) 62 | } 63 | 64 | func (m *MySqlAdapter) Ping() error { 65 | sqlDB, err := m.db.DB() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | ctx, cFunc := context.WithTimeout(context.Background(), 2*time.Second) 71 | defer cFunc() 72 | 73 | return sqlDB.PingContext(ctx) 74 | } 75 | -------------------------------------------------------------------------------- /services/pkg/common/db/adapters/sql_adapter.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type SqlDataAdapter interface { 8 | GetDB() (*gorm.DB, error) 9 | Migrate(...interface{}) error 10 | Ping() error 11 | } 12 | -------------------------------------------------------------------------------- /services/pkg/common/db/migration.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db/adapters" 7 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db/models" 8 | ) 9 | 10 | func MigrateSqlModels(adapter adapters.SqlDataAdapter) error { 11 | db, err := adapter.GetDB() 12 | if err != nil { 13 | return err 14 | } 15 | 16 | log.Printf("Running schema migration on DB:%s", db.Migrator().CurrentDatabase()) 17 | return adapter.Migrate(&models.Vulnerability{}) 18 | } 19 | -------------------------------------------------------------------------------- /services/pkg/common/db/models/vulnerability.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/datatypes" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | const ( 11 | VulnerabilitySchemaTypeOpenSSF = "OpenSSF" 12 | VulnerabilitySourceOpenSSF = VulnerabilitySchemaTypeOpenSSF 13 | ) 14 | 15 | type Vulnerability struct { 16 | gorm.Model 17 | 18 | // Lookup key, must be unique (Composite index on MySQL 8 must be < 3072/4 with utf8mb4) 19 | Ecosystem string `gorm:"type:varchar(32);not null;index:lookup_idx;priority:1"` 20 | Group string `gorm:"type:varchar(512);not null;index:lookup_idx;priority:2"` 21 | Name string `gorm:"type:varchar(128);not null;index:lookup_idx;priority:3"` 22 | 23 | // Meta data to deserialize Data into appropriate vulnerability model 24 | SchemaType string `gorm:"type:varchar(32);not null"` 25 | SchemaVersion string `gorm:"type:varchar(32);not null"` 26 | 27 | // Common vulnerability data 28 | ExternalSource string `gorm:"type:varchar(32);not null;uniqueIndex:external_src_idx;priority:1"` 29 | ExternalId string `gorm:"type:varchar(128);unique;not null;uniqueIndex:external_src_idx;priority:2"` 30 | Title string `gorm:"not null"` 31 | Description string `gorm:"not null"` 32 | 33 | // JSON serialized vulnerability data in SchemaType/SchemaVersion format 34 | Data datatypes.JSON `gorm:"not null;size:2097152"` 35 | 36 | // Timestamp for vulnerability data from external sources 37 | DataModifiedAt time.Time `gorm:"not null"` 38 | DataPublishedAt time.Time `gorm:"not null"` 39 | } 40 | -------------------------------------------------------------------------------- /services/pkg/common/db/repository.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db/adapters" 5 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db/models" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type VulnerabilityRepository struct { 10 | adapter adapters.SqlDataAdapter 11 | } 12 | 13 | func NewVulnerabilityRepository(adapter adapters.SqlDataAdapter) (*VulnerabilityRepository, error) { 14 | return &VulnerabilityRepository{adapter: adapter}, nil 15 | } 16 | 17 | func (r *VulnerabilityRepository) Upsert(vulnerability models.Vulnerability) error { 18 | db, err := r.adapter.GetDB() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | err = db.Transaction(func(tx *gorm.DB) error { 24 | var records []models.Vulnerability 25 | ntx := db.Where(&models.Vulnerability{ 26 | ExternalSource: vulnerability.ExternalSource, 27 | ExternalId: vulnerability.ExternalId, 28 | }).Find(&records) 29 | 30 | if ntx.Error == nil && len(records) > 0 { 31 | if records[0].DataModifiedAt.Unix() < vulnerability.DataModifiedAt.Unix() { 32 | vulnerability.ID = records[0].ID 33 | vulnerability.CreatedAt = records[0].CreatedAt 34 | ntx = db.Save(&vulnerability) 35 | } 36 | } else { 37 | ntx = db.Create(&vulnerability) 38 | } 39 | 40 | return ntx.Error 41 | }) 42 | 43 | return err 44 | } 45 | 46 | func (r *VulnerabilityRepository) Lookup(ecosystem, group, name string) ([]models.Vulnerability, error) { 47 | vulnerabilities := make([]models.Vulnerability, 0) 48 | db, err := r.adapter.GetDB() 49 | if err != nil { 50 | return vulnerabilities, err 51 | } 52 | 53 | tx := db.Where(&models.Vulnerability{ 54 | Ecosystem: ecosystem, 55 | Group: group, 56 | Name: name, 57 | }).Find(&vulnerabilities) 58 | 59 | return vulnerabilities, tx.Error 60 | } 61 | -------------------------------------------------------------------------------- /services/pkg/common/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | var ( 8 | defaultLogger = zap.NewNop() 9 | sugarLogger = defaultLogger.Sugar() 10 | ) 11 | 12 | // Init logger for a service 13 | // There are two type of logging config: 14 | // 1. Service specific 15 | // 2. Common config 16 | // Common config helps in logging consistency across all services 17 | // in an environment 18 | func Init(svc string) { 19 | l, err := zapBuild(zapConfig()) 20 | if err != nil { 21 | panic("Failed to build logger") 22 | } 23 | 24 | defaultLogger = l.With(zap.String("service", svc)) 25 | sugarLogger = defaultLogger.Sugar() 26 | } 27 | 28 | func zapConfig() zap.Config { 29 | return zap.NewDevelopmentConfig() 30 | } 31 | 32 | func zapBuild(config zap.Config) (*zap.Logger, error) { 33 | return config.Build(zap.AddCallerSkip(1)) 34 | } 35 | 36 | func Infof(msg string, args ...any) { 37 | sugarLogger.Infof(msg, args...) 38 | } 39 | 40 | func Warnf(msg string, args ...any) { 41 | sugarLogger.Infof(msg, args...) 42 | } 43 | 44 | func Errorf(msg string, args ...any) { 45 | sugarLogger.Errorf(msg, args...) 46 | } 47 | 48 | func Fatalf(msg string, args ...any) { 49 | sugarLogger.Fatalf(msg, args...) 50 | } 51 | 52 | func Debugf(msg string, args ...any) { 53 | sugarLogger.Debugf(msg, args...) 54 | } 55 | 56 | func With(args map[string]any) *zap.SugaredLogger { 57 | var fields []zap.Field 58 | for key, value := range args { 59 | fields = append(fields, zap.Any(key, value)) 60 | } 61 | 62 | return defaultLogger.With(fields...).WithOptions(zap.AddCallerSkip(1)).Sugar() 63 | } 64 | 65 | func WithRequestID(id string) *zap.SugaredLogger { 66 | return With(map[string]any{"request-id": id}) 67 | } 68 | -------------------------------------------------------------------------------- /services/pkg/common/messaging/messaging.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 8 | ) 9 | 10 | type MessagingQueueSubscription interface { 11 | Unsubscribe() error 12 | } 13 | 14 | type MessagingService interface { 15 | QueueSubscribe(topic string, group string, handler func(msg interface{})) (MessagingQueueSubscription, error) 16 | Publish(topic string, msg interface{}) error 17 | } 18 | 19 | func NewService(adapter *config_api.MessagingAdapter) (MessagingService, error) { 20 | switch adapter.Type { 21 | case config_api.MessagingAdapter_NATS: 22 | return NewNatsMessagingService(adapter) 23 | case config_api.MessagingAdapter_KAFKA: 24 | return NewKafkaProtobufMessagingService(strings.Join(adapter.GetKafka().GetBootstrapServers(), ","), 25 | adapter.GetKafka().SchemaRegistryUrl) 26 | default: 27 | return nil, fmt.Errorf("no messaging adapter for: %s", adapter.Type.String()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services/pkg/common/messaging/messaging_kafka_protobuf.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/confluentinc/confluent-kafka-go/kafka" 9 | "github.com/confluentinc/confluent-kafka-go/schemaregistry" 10 | "github.com/confluentinc/confluent-kafka-go/schemaregistry/serde" 11 | "github.com/confluentinc/confluent-kafka-go/schemaregistry/serde/protobuf" 12 | ) 13 | 14 | type kafkaMessagingService struct { 15 | producer *kafka.Producer 16 | serializer *protobuf.Serializer 17 | deliveryChannel chan kafka.Event 18 | } 19 | 20 | func NewKafkaProtobufMessagingService(bootstrapServers, schemaRegistryUrl string) (MessagingService, error) { 21 | log.Printf("Kafka msg service init with bootstrap:%s registry:%s", 22 | bootstrapServers, schemaRegistryUrl) 23 | 24 | producer, err := kafka.NewProducer(&kafka.ConfigMap{"bootstrap.servers": bootstrapServers}) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | registryClient, err := schemaregistry.NewClient(schemaregistry.NewConfig(schemaRegistryUrl)) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | protobufSerializer, err := protobuf.NewSerializer(registryClient, serde.ValueSerde, protobuf.NewSerializerConfig()) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | messageDeliveryNotificationChan := make(chan kafka.Event, 100000) 40 | messagingService := &kafkaMessagingService{ 41 | producer: producer, 42 | serializer: protobufSerializer, 43 | deliveryChannel: messageDeliveryNotificationChan, 44 | } 45 | 46 | go messagingService.deliveryEventHandler() 47 | return messagingService, nil 48 | } 49 | 50 | func (svc *kafkaMessagingService) deliveryEventHandler() { 51 | log.Printf("Starting Kafka (protobuf) messaging service delivery event handler") 52 | for msg := range svc.deliveryChannel { 53 | m, ok := msg.(*kafka.Message) 54 | if !ok { 55 | log.Printf("[ERROR] Failed to cast msg to kafka.Message in delivery channel handler") 56 | continue 57 | } 58 | 59 | if m.TopicPartition.Error != nil { 60 | log.Printf("Failed to deliver msg: %v", m.TopicPartition.Error) 61 | } 62 | } 63 | 64 | log.Printf("[ERROR] Kafka msg deliver handler QUIT") 65 | } 66 | 67 | func (svc *kafkaMessagingService) QueueSubscribe(topic string, group string, handler func(msg interface{})) (MessagingQueueSubscription, error) { 68 | return nil, errors.New("queue subscription is not supported yet") 69 | } 70 | 71 | func (svc *kafkaMessagingService) Publish(topic string, msg interface{}) error { 72 | payload, err := svc.serializer.Serialize(topic, msg) 73 | if err != nil { 74 | return fmt.Errorf("Failed to serialize payload: %v", err) 75 | } 76 | 77 | return svc.producer.Produce(&kafka.Message{ 78 | TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny}, 79 | Value: payload, 80 | Headers: []kafka.Header{}, 81 | }, svc.deliveryChannel) 82 | } 83 | -------------------------------------------------------------------------------- /services/pkg/common/messaging/messaging_nats.go: -------------------------------------------------------------------------------- 1 | package messaging 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/nats-io/nats.go" 9 | 10 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 11 | ) 12 | 13 | type natsMessagingService struct { 14 | connection *nats.Conn 15 | jsonEncodedConnection *nats.EncodedConn 16 | } 17 | 18 | func NewNatsMessagingService(cfg *config_api.MessagingAdapter) (MessagingService, error) { 19 | certs := nats.ClientCert(os.Getenv("SERVICE_TLS_CERT"), os.Getenv("SERVICE_TLS_KEY")) 20 | rootCA := nats.RootCAs(os.Getenv("SERVICE_TLS_ROOT_CA")) 21 | 22 | log.Printf("Initializing new nats connection with: %s", cfg) 23 | conn, err := nats.Connect(cfg.GetNats().Url, 24 | nats.RetryOnFailedConnect(true), 25 | nats.MaxReconnects(5), 26 | nats.ReconnectWait(1*time.Second), 27 | certs, rootCA) 28 | 29 | if err != nil { 30 | return &natsMessagingService{}, err 31 | } 32 | 33 | err = conn.Flush() 34 | if err != nil { 35 | return &natsMessagingService{}, err 36 | } 37 | 38 | rtt, err := conn.RTT() 39 | if err != nil { 40 | return &natsMessagingService{}, err 41 | } 42 | 43 | log.Printf("NATS server connection initialized with RTT=%s", rtt) 44 | 45 | jsonEncodedConn, err := nats.NewEncodedConn(conn, nats.JSON_ENCODER) 46 | if err != nil { 47 | return &natsMessagingService{}, err 48 | } 49 | 50 | return &natsMessagingService{connection: conn, 51 | jsonEncodedConnection: jsonEncodedConn}, nil 52 | } 53 | 54 | func (svc *natsMessagingService) QueueSubscribe(topic string, group string, handler func(msg interface{})) (MessagingQueueSubscription, error) { 55 | return svc.jsonEncodedConnection.QueueSubscribe(topic, group, handler) 56 | } 57 | 58 | func (svc *natsMessagingService) Publish(topic string, msg interface{}) error { 59 | return svc.jsonEncodedConnection.Publish(topic, msg) 60 | } 61 | -------------------------------------------------------------------------------- /services/pkg/common/models/artefact_utils.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/openssf" 7 | ) 8 | 9 | func NewArtefact(src ArtefactSource, name, group, version string) Artefact { 10 | return Artefact{ 11 | Source: src, 12 | Name: name, 13 | Group: group, 14 | Version: version, 15 | } 16 | } 17 | 18 | func (a Artefact) OpenSsfEcosystem() string { 19 | if a.Source.Type == ArtefactSourceTypeMaven2 { 20 | return openssf.VulnerabilityEcosystemMaven 21 | } else if a.Source.Type == ArtefactSourceTypePypi { 22 | return openssf.VulnerabilityEcosystemPypi 23 | } else if a.Source.Type == ArtefactSourceTypeNpm { 24 | return openssf.VulnerabilityEcosystemNpm 25 | } else if a.Source.Type == ArtefactSourceTypeRubyGems { 26 | return openssf.VulnerabilityEcosystemRubyGems 27 | } 28 | 29 | return "" 30 | } 31 | 32 | func (a Artefact) OpenSsfPackageName() string { 33 | if a.Source.Type == ArtefactSourceTypeMaven2 { 34 | return fmt.Sprintf("%s:%s", a.Group, a.Name) 35 | } 36 | 37 | return a.Name 38 | } 39 | 40 | func (a Artefact) OpenSsfPackageVersion() string { 41 | return a.Version 42 | } 43 | -------------------------------------------------------------------------------- /services/pkg/common/models/event_utils.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | event_api "github.com/abhisek/supply-chain-gateway/services/gen" 8 | 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 10 | ) 11 | 12 | func (m MetaEventWithAttributes) Serialize() ([]byte, error) { 13 | bytes, err := json.Marshal(m) 14 | if err != nil { 15 | return []byte{}, err 16 | } else { 17 | return bytes, nil 18 | } 19 | } 20 | 21 | func newMetaEventWithAttributes(t string) MetaEventWithAttributes { 22 | return MetaEventWithAttributes{ 23 | MetaEvent: MetaEvent{ 24 | Type: t, 25 | Version: EventSchemaVersion, 26 | }, 27 | MetaAttributes: MetaAttributes{}, 28 | } 29 | } 30 | 31 | func NewArtefactRequestEvent(a Artefact) DomainEvent[Artefact] { 32 | return DomainEvent[Artefact]{ 33 | MetaEventWithAttributes: newMetaEventWithAttributes(EventTypeArtefactRequestSubject), 34 | Data: a, 35 | } 36 | } 37 | 38 | func NewArtefactResponseEvent(a Artefact) DomainEvent[Artefact] { 39 | return DomainEvent[Artefact]{ 40 | MetaEventWithAttributes: newMetaEventWithAttributes(EventTypeArtefactResponseSubject), 41 | Data: a, 42 | } 43 | } 44 | 45 | // Utils for new spec driven events 46 | func eventUid() string { 47 | return utils.NewUniqueId() 48 | } 49 | 50 | func eventTimestamp(ts time.Time) *event_api.EventTimestamp { 51 | return &event_api.EventTimestamp{ 52 | Seconds: ts.Unix(), 53 | Nanos: int32(ts.Nanosecond()), 54 | } 55 | } 56 | 57 | func NewSpecEventHeader(tp event_api.EventType, source string) *event_api.EventHeader { 58 | return &event_api.EventHeader{ 59 | Type: tp, 60 | Source: source, 61 | Id: eventUid(), 62 | Context: &event_api.EventContext{}, 63 | } 64 | } 65 | 66 | func NewSpecHeaderWithContext(tp event_api.EventType, source string, ctx *event_api.EventContext) *event_api.EventHeader { 67 | eh := NewSpecEventHeader(tp, source) 68 | eh.Context = ctx 69 | 70 | return eh 71 | } 72 | -------------------------------------------------------------------------------- /services/pkg/common/models/events.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | const ( 4 | EventSchemaVersion = "1.0.0" 5 | EventTypeDomainEvent = "event.domain" 6 | EventTypeArtefactRequestSubject = "event.artefact.request" 7 | EventTypeArtefactResponseSubject = "event.artefact.response" 8 | 9 | DomainEventTypeCreated = "event.type.created" 10 | DomainEventTypeUpdated = "event.type.updated" 11 | DomainEventTypeDeleted = "event.type.deleted" 12 | ) 13 | 14 | // LEGACY event: Move to spec based events 15 | 16 | type MetaEvent struct { 17 | Version string `json:"version"` 18 | Type string `json:"type"` 19 | } 20 | 21 | type MetaAttributes struct { 22 | Attributes map[string]string `json:"attributes"` 23 | } 24 | 25 | type MetaEventWithAttributes struct { 26 | MetaEvent 27 | MetaAttributes 28 | } 29 | 30 | type DomainEvent[T any] struct { 31 | MetaEventWithAttributes `json:"meta"` 32 | Data T `json:"data"` 33 | } 34 | 35 | type DomainEventBuilder[T any] interface { 36 | Created(v T) DomainEvent[T] 37 | Updated(v T) DomainEvent[T] 38 | Deleted(v T) DomainEvent[T] 39 | From(v interface{}) (DomainEvent[T], error) 40 | } 41 | -------------------------------------------------------------------------------- /services/pkg/common/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 5 | ) 6 | 7 | var ( 8 | ArtefactSourceTypeMaven2 = config_api.GatewayUpstreamType_Maven.String() 9 | ArtefactSourceTypeNpm = config_api.GatewayUpstreamType_Npm.String() 10 | ArtefactSourceTypePypi = config_api.GatewayUpstreamType_PyPI.String() 11 | ArtefactSourceTypeRubyGems = config_api.GatewayUpstreamType_RubyGems.String() 12 | 13 | ArtefactLicenseTypeSpdx = "SPDX" 14 | ArtefactLicenseTypeCycloneDx = "CycloneDX" 15 | 16 | ArtefactVulnerabilitySeverityCritical = "CRITICAL" 17 | ArtefactVulnerabilitySeverityHigh = "HIGH" 18 | ArtefactVulnerabilitySeverityMedium = "MEDIUM" 19 | ArtefactVulnerabilitySeverityLow = "LOW" 20 | ArtefactVulnerabilitySeverityInfo = "INFO" 21 | 22 | ArtefactVulnerabilityScoreTypeCVSSv3 = "CVSSv3" 23 | ) 24 | 25 | type ArtefactRepositoryAuthentication struct { 26 | Type string `yaml:"type"` 27 | } 28 | 29 | type ArtefactUpstreamAuthentication struct { 30 | Type string `yaml:"type"` 31 | Provider string `yaml:"provider"` 32 | } 33 | 34 | type ArtefactRepository struct { 35 | Host string `yaml:"host"` 36 | Port int16 `yaml:"port"` 37 | Tls bool `yaml:"tls"` 38 | Sni string `yaml:"sni"` 39 | Authentication ArtefactRepositoryAuthentication `yaml:"authentication"` 40 | } 41 | 42 | type ArtefactRoutingRule struct { 43 | Prefix string `yaml:"prefix"` 44 | Host string `yaml:"host"` 45 | } 46 | 47 | type ArtefactUpStream struct { 48 | Name string `yaml:"name"` 49 | Type string `yaml:"type"` 50 | RoutingRule ArtefactRoutingRule `yaml:"route"` 51 | Repository ArtefactRepository `yaml:"repository"` 52 | Authentication ArtefactUpstreamAuthentication `yaml:"authentication"` 53 | } 54 | 55 | type ArtefactSource struct { 56 | Type string `json:"type"` 57 | } 58 | 59 | // Align with CVSS v3 but keep room 60 | type ArtefactVulnerabilityScore struct { 61 | Type string `json:"type"` 62 | Value string `json:"value"` 63 | } 64 | 65 | // Align with CVE but keep room for enhancement 66 | type ArtefactVulnerabilityId struct { 67 | Source string `json:"source"` 68 | Id string `json:"id"` 69 | } 70 | 71 | type ArtefactVulnerability struct { 72 | Name string `json:"name"` 73 | Id ArtefactVulnerabilityId `json:"id"` 74 | Severity string `json:"severity"` 75 | Scores []ArtefactVulnerabilityScore `json:"scores"` 76 | } 77 | 78 | // Align with SPDX / CycloneDX 79 | type ArtefactLicense struct { 80 | Type string `json:"type"` // SPDX | CyloneDX 81 | Id string `json:"id"` // SPDX or CycloneDX ID 82 | Name string `json:"name"` // Human Readable Name 83 | } 84 | 85 | type Artefact struct { 86 | Source ArtefactSource `json:"source"` 87 | Group string `json:"group"` 88 | Name string `json:"name"` 89 | Version string `json:"version"` 90 | } 91 | -------------------------------------------------------------------------------- /services/pkg/common/models/upstream_utils.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 11 | ) 12 | 13 | var ( 14 | errIncorrectPrefix = errors.New("incorrect path prefix") 15 | errIncorrectMaven2Path = errors.New("incorrect maven2 path") 16 | errIncorrectPypiPath = errors.New("incorrect pypi path") 17 | errUnimplementedUpstreamType = errors.New("path resolver for upstream type is not implemented") 18 | ) 19 | 20 | func GetUpstreamByHostAndPath(host, path string) (ArtefactUpStream, error) { 21 | upstreams := config.Upstreams() 22 | 23 | for _, us := range upstreams { 24 | upstream := ToUpstream(us) 25 | 26 | if upstream.MatchHost(host) && upstream.MatchPath(path) { 27 | return upstream, nil 28 | } 29 | } 30 | 31 | return ArtefactUpStream{}, fmt.Errorf("no upstream resolved using %s/%s", host, path) 32 | } 33 | 34 | func GetArtefactByHostAndPath(host, path string) (Artefact, error) { 35 | upstreams := config.Upstreams() 36 | 37 | for _, us := range upstreams { 38 | upstream := ToUpstream(us) 39 | 40 | if upstream.MatchHost(host) && upstream.MatchPath(path) { 41 | return upstream.Path2Artefact(path) 42 | } 43 | } 44 | 45 | return Artefact{}, fmt.Errorf("no artefact resolved using %s/%s", host, path) 46 | } 47 | 48 | func (s ArtefactUpStream) NeedAuthentication() bool { 49 | return s.Authentication.Type != config_api.GatewayAuthenticationType_NoAuth.String() 50 | } 51 | 52 | func (s ArtefactUpStream) NeedUpstreamAuthentication() bool { 53 | return s.Repository.Authentication.Type != config_api.GatewayAuthenticationType_NoAuth.String() 54 | } 55 | 56 | func (s ArtefactUpStream) MatchHost(host string) bool { 57 | return (utils.IsEmptyString(s.RoutingRule.Host)) || (s.RoutingRule.Host == host) 58 | } 59 | 60 | func (s ArtefactUpStream) MatchPath(path string) bool { 61 | path = utils.CleanPath(path) 62 | return strings.HasPrefix(path, s.RoutingRule.Prefix) 63 | } 64 | 65 | func (s ArtefactUpstreamAuthentication) IsBasic() bool { 66 | return s.Type == config_api.GatewayAuthenticationType_Basic.String() 67 | } 68 | 69 | func (s ArtefactUpstreamAuthentication) IsNoAuth() bool { 70 | return s.Type == config_api.GatewayAuthenticationType_NoAuth.String() 71 | } 72 | 73 | // Resolve an HTTP request path for this artefact into an Artefact model 74 | func (s ArtefactUpStream) Path2Artefact(path string) (Artefact, error) { 75 | path = utils.CleanPath(path) 76 | if !strings.HasPrefix(path, s.RoutingRule.Prefix) { 77 | return Artefact{}, errIncorrectPrefix 78 | } 79 | 80 | path = strings.TrimPrefix(path, s.RoutingRule.Prefix) 81 | if path != "" && path[0] == '/' { 82 | path = path[1:] 83 | } 84 | 85 | parts := strings.Split(path, "/") 86 | switch s.Type { 87 | case ArtefactSourceTypeMaven2: 88 | return artefactForMaven2(parts) 89 | case ArtefactSourceTypePypi: 90 | return artefactForPypi(parts) 91 | default: 92 | return Artefact{}, errUnimplementedUpstreamType 93 | } 94 | } 95 | 96 | // Stop gap method to map a spec based upstream into legacy upstream 97 | func ToUpstream(us *config_api.GatewayUpstream) ArtefactUpStream { 98 | upstream := ArtefactUpStream{ 99 | Name: us.GetName(), 100 | Type: us.GetType().String(), 101 | RoutingRule: ArtefactRoutingRule{ 102 | Prefix: us.GetRoute().GetPathPrefix(), 103 | Host: us.GetRoute().GetHost(), 104 | }, 105 | Authentication: ArtefactUpstreamAuthentication{ 106 | Type: us.GetAuthentication().GetType().String(), 107 | Provider: us.GetAuthentication().GetProvider(), 108 | }, 109 | } 110 | 111 | return upstream 112 | } 113 | 114 | func artefactForPypi(parts []string) (Artefact, error) { 115 | if len(parts) == 0 { 116 | return Artefact{}, errIncorrectPypiPath 117 | } 118 | 119 | if ((parts[0] == "simple") || (parts[0] == "packages")) && (len(parts) >= 2) { 120 | parts = parts[1:] 121 | } 122 | 123 | name := parts[0] 124 | version := "" 125 | 126 | if len(parts) > 1 { 127 | version = parts[1] 128 | } 129 | 130 | return NewArtefact(ArtefactSource{Type: ArtefactSourceTypePypi}, 131 | name, "", version), nil 132 | } 133 | 134 | func artefactForMaven2(parts []string) (Artefact, error) { 135 | if len(parts) < 4 { 136 | return Artefact{}, errIncorrectMaven2Path 137 | } 138 | 139 | // Ignore the filename 140 | _ = parts[:len(parts)-1] 141 | 142 | version := parts[len(parts)-2] 143 | name := parts[len(parts)-3] 144 | 145 | parts = parts[:len(parts)-3] 146 | group := strings.Join(parts, ".") 147 | 148 | return NewArtefact(ArtefactSource{Type: ArtefactSourceTypeMaven2}, 149 | name, group, version), nil 150 | } 151 | -------------------------------------------------------------------------------- /services/pkg/common/models/upstream_utils_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPath2Artefact(t *testing.T) { 10 | cases := []struct { 11 | // Input 12 | prefix string 13 | upType string 14 | path string 15 | 16 | // Output 17 | group, name, version string 18 | err error 19 | }{ 20 | { 21 | "/maven2", ArtefactSourceTypeMaven2, "/maven2/com/google/guava/guava/30.1.1-jre/guava-30.1.1-jre.pom", 22 | "com.google.guava", "guava", "30.1.1-jre", nil, 23 | }, 24 | { 25 | "/", ArtefactSourceTypeMaven2, "/com/google/guava/guava/30.1.1-jre/guava-30.1.1-jre.pom", 26 | "com.google.guava", "guava", "30.1.1-jre", nil, 27 | }, 28 | { 29 | "/maven2", ArtefactSourceTypeMaven2, "/maven2/com/google/guava", 30 | "", "", "", errIncorrectMaven2Path, 31 | }, 32 | { 33 | "/maven2", ArtefactSourceTypeMaven2, "", 34 | "", "", "", errIncorrectPrefix, 35 | }, 36 | { 37 | "/maven2", ArtefactSourceTypeMaven2, "/maven2", 38 | "", "", "", errIncorrectMaven2Path, 39 | }, 40 | { 41 | "/maven2", ArtefactSourceTypeMaven2, "/maven2/////", 42 | "", "", "", errIncorrectMaven2Path, 43 | }, 44 | { 45 | "/maven2", ArtefactSourceTypeMaven2, "/maven2/com/google/guava/guava/../guava2/30.1.1-jre/guava-30.1.1-jre.pom", 46 | "com.google.guava", "guava2", "30.1.1-jre", nil, 47 | }, 48 | { 49 | "/maven2", ArtefactSourceTypeMaven2, "/maven2/com/google/guava/guava/../../../../../m/x/y/z", 50 | "", "", "", errIncorrectPrefix, 51 | }, 52 | } 53 | 54 | for _, test := range cases { 55 | upstream := ArtefactUpStream{ 56 | Type: test.upType, 57 | RoutingRule: ArtefactRoutingRule{ 58 | Prefix: test.prefix, 59 | }, 60 | } 61 | 62 | artefact, err := upstream.Path2Artefact(test.path) 63 | 64 | if test.err != nil { 65 | assert.NotNil(t, err) 66 | if err != nil { 67 | assert.Equal(t, test.err.Error(), err.Error()) 68 | } 69 | 70 | } else { 71 | assert.Nil(t, err) 72 | assert.Equal(t, test.group, artefact.Group) 73 | assert.Equal(t, test.name, artefact.Name) 74 | assert.Equal(t, test.version, artefact.Version) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /services/pkg/common/obs/tracing.go: -------------------------------------------------------------------------------- 1 | package obs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 11 | "go.opentelemetry.io/otel" 12 | "go.opentelemetry.io/otel/attribute" 13 | "go.opentelemetry.io/otel/codes" 14 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace" 15 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 16 | "go.opentelemetry.io/otel/propagation" 17 | "go.opentelemetry.io/otel/sdk/resource" 18 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 19 | "go.opentelemetry.io/otel/trace" 20 | ) 21 | 22 | const ( 23 | tracerControlEnvKey = "APP_SERVICE_OBS_ENABLED" 24 | tracerServiceNameEnvKey = "APP_SERVICE_NAME" 25 | tracerServiceEnvEnvKey = "APP_SERVICE_ENV" 26 | tracerServiceLabelEnvKey = "APP_SERVICE_LABELS" 27 | tracerOtelExporterUrlEnvKey = "APP_OTEL_EXPORTER_OTLP_ENDPOINT" 28 | ) 29 | 30 | var ( 31 | globalTracer = otel.Tracer("NOP") 32 | ) 33 | 34 | func InitTracing() func(context.Context) error { 35 | if !isTracingEnabled() { 36 | logger.Infof("Tracer is disabled") 37 | return func(ctx context.Context) error { return nil } 38 | } 39 | 40 | serviceName := os.Getenv(tracerServiceNameEnvKey) 41 | serviceEnv := os.Getenv(tracerServiceEnvEnvKey) 42 | otlpExporterUrl := os.Getenv(tracerOtelExporterUrlEnvKey) 43 | 44 | if utils.IsEmptyString(serviceName) || utils.IsEmptyString(otlpExporterUrl) { 45 | panic("tracer is enable but required environment is not defined") 46 | } 47 | 48 | logger.With(map[string]any{ 49 | "ServiceName": serviceName, 50 | "ServiceEnv": serviceEnv, 51 | "OtlpExporterUrl": otlpExporterUrl, 52 | }).Infof("Enabling tracer") 53 | 54 | // NOTE: We expect the collector to be a sidecar 55 | // TODO: Revisit this for using a secure channel 56 | exporter, err := otlptrace.New( 57 | context.Background(), 58 | otlptracegrpc.NewClient( 59 | otlptracegrpc.WithInsecure(), 60 | otlptracegrpc.WithEndpoint(otlpExporterUrl), 61 | ), 62 | ) 63 | 64 | if err != nil { 65 | panic(fmt.Sprintf("error creating otlp exporter: %v", err)) 66 | } 67 | 68 | resources, err := resource.New( 69 | context.Background(), 70 | resource.WithAttributes( 71 | attribute.String("service.name", serviceName), 72 | attribute.String("service.environment", serviceEnv), 73 | attribute.String("service.language", "go"), 74 | ), 75 | ) 76 | 77 | if err != nil { 78 | panic(fmt.Sprintf("error creating otlp resource: %v", err)) 79 | } 80 | 81 | otel.SetTracerProvider( 82 | sdktrace.NewTracerProvider( 83 | sdktrace.WithSampler(sdktrace.AlwaysSample()), 84 | sdktrace.WithBatcher(exporter), 85 | sdktrace.WithResource(resources), 86 | ), 87 | ) 88 | 89 | otel.SetTextMapPropagator(propagation.TraceContext{}) 90 | globalTracer = otel.Tracer(serviceName) 91 | 92 | logger.Infof("Global tracer enabled") 93 | return exporter.Shutdown 94 | } 95 | 96 | func ShutdownTracing() { 97 | // Explicitly flush and shutdown tracers 98 | } 99 | 100 | func Spanned(current context.Context, name string, 101 | f func(context.Context) error) error { 102 | newCtx, span := globalTracer.Start(current, name) 103 | defer span.End() 104 | 105 | err := f(newCtx) 106 | if err != nil { 107 | span.RecordError(err) 108 | span.SetStatus(codes.Error, err.Error()) 109 | } 110 | 111 | return err 112 | } 113 | 114 | func SetSpanAttribute(ctx context.Context, key string, value string) { 115 | span := trace.SpanFromContext(ctx) 116 | span.SetAttributes(attribute.KeyValue{ 117 | Key: attribute.Key(key), 118 | Value: attribute.StringValue(value), 119 | }) 120 | } 121 | 122 | func LoggerTags(ctx context.Context) map[string]any { 123 | tags := map[string]any{} 124 | span := trace.SpanFromContext(ctx) 125 | 126 | if span.IsRecording() { 127 | tags["span_id"] = span.SpanContext().SpanID() 128 | tags["trace_id"] = span.SpanContext().TraceID() 129 | tags["trace_flags"] = span.SpanContext().TraceFlags() 130 | } 131 | 132 | return tags 133 | } 134 | 135 | func isTracingEnabled() bool { 136 | bRet, err := strconv.ParseBool(os.Getenv(tracerControlEnvKey)) 137 | if err != nil { 138 | return false 139 | } 140 | 141 | return bRet 142 | } 143 | -------------------------------------------------------------------------------- /services/pkg/common/openssf/osv.go: -------------------------------------------------------------------------------- 1 | package openssf 2 | 3 | // https://ossf.github.io/osv-schema/ 4 | const ( 5 | VulnerabilityEcosystemMaven = "Maven" 6 | VulnerabilityEcosystemNpm = "npm" 7 | VulnerabilityEcosystemPypi = "PyPI" 8 | VulnerabilityEcosystemRubyGems = "RubyGems" 9 | ) 10 | -------------------------------------------------------------------------------- /services/pkg/common/openssf/osv_adapter.go: -------------------------------------------------------------------------------- 1 | package openssf 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 11 | "github.com/gojek/heimdall/v7" 12 | "github.com/gojek/heimdall/v7/hystrix" 13 | ) 14 | 15 | const ( 16 | OsvQueryEndpoint = "https://api.osv.dev/v1/query" 17 | ) 18 | 19 | type OsvServiceAdapterConfig struct { 20 | Timeout time.Duration 21 | Retry int 22 | MaxConcurrentRequest int 23 | } 24 | 25 | func DefaultServiceAdapterConfig() OsvServiceAdapterConfig { 26 | return OsvServiceAdapterConfig{ 27 | Timeout: 5 * time.Second, 28 | Retry: 3, 29 | MaxConcurrentRequest: 5, 30 | } 31 | } 32 | 33 | type OsvServiceAdapter struct { 34 | config OsvServiceAdapterConfig 35 | client *hystrix.Client 36 | } 37 | 38 | func NewOsvServiceAdapter(config OsvServiceAdapterConfig) *OsvServiceAdapter { 39 | backoff := heimdall.NewConstantBackoff(2*time.Second, 100*time.Millisecond) 40 | 41 | client := hystrix.NewClient( 42 | hystrix.WithHTTPTimeout(config.Timeout), 43 | hystrix.WithCommandName("osv_api_get_request"), 44 | hystrix.WithMaxConcurrentRequests(config.MaxConcurrentRequest), 45 | hystrix.WithRetryCount(config.Retry), 46 | hystrix.WithRetrier(heimdall.NewRetrier(backoff)), 47 | ) 48 | 49 | return &OsvServiceAdapter{config: config, client: client} 50 | } 51 | 52 | func (svc *OsvServiceAdapter) QueryPackage(ecosystem, name, version string) (V1VulnerabilityList, error) { 53 | rQuery := &V1Query{ 54 | Package: &struct { 55 | OsvPackage "yaml:\",inline\"" 56 | }{}, 57 | } 58 | 59 | rQuery.Version = &version 60 | rQuery.Package.Ecosystem = &ecosystem 61 | rQuery.Package.Name = &name 62 | 63 | logger.Debugf("Querying OSV with: ecosystem:%s name:%s version:%s", 64 | ecosystem, name, version) 65 | 66 | body, err := json.Marshal(rQuery) 67 | if err != nil { 68 | return V1VulnerabilityList{}, err 69 | } 70 | 71 | resp, err := svc.client.Post(OsvQueryEndpoint, bytes.NewReader(body), http.Header{}) 72 | if err != nil { 73 | return V1VulnerabilityList{}, err 74 | } 75 | 76 | if resp.StatusCode != http.StatusOK { 77 | return V1VulnerabilityList{}, fmt.Errorf("unexpected http status code: %d", resp.StatusCode) 78 | } 79 | 80 | var vulnList V1VulnerabilityList 81 | err = json.NewDecoder(resp.Body).Decode(&vulnList) 82 | if err != nil { 83 | return V1VulnerabilityList{}, fmt.Errorf("failed to decoded to vulnerability list: %w", err) 84 | } 85 | 86 | // No result found 87 | if vulnList.Vulns == nil { 88 | return V1VulnerabilityList{}, fmt.Errorf("empty vulnerability list from OSV") 89 | } 90 | 91 | return vulnList, nil 92 | } 93 | -------------------------------------------------------------------------------- /services/pkg/common/openssf/scorecard.go: -------------------------------------------------------------------------------- 1 | package openssf 2 | 3 | const ( 4 | ScBinaryArtifactsCheck = "binary_artifacts" 5 | ScBranchProtectionCheck = "branch_protection" 6 | ScCiiBestPracticeCheck = "cii_best_practices" 7 | ScCodeReviewCheck = "code_review" 8 | ScDangerousWorkflowCheck = "dangerous_workflow" 9 | ScDependencyUpdateToolCheck = "dependency_update_tool" 10 | ScFuzzingCheck = "fuzzing" 11 | ScLicenseCheck = "license" 12 | ScMaintainedCheck = "maintained" 13 | ScPackagingCheck = "packaging" 14 | ScPinnedDependenciesCheck = "pinned_dependencies" 15 | ScSastCheck = "sast" 16 | ScSecurityPolicyCheck = "security_policy" 17 | ScSignedReleasesCheck = "signed_releases" 18 | ScVulnerabilitiesCheck = "vulnerabilities" 19 | ScTokenPermissionsCheck = "token_permissions" 20 | ) 21 | 22 | type ProjectScorecardCheck struct { 23 | Reason string `json:"reason"` 24 | Score float32 `json:"score"` 25 | } 26 | 27 | type ProjectScorecardRepo struct { 28 | Name string `json:"name"` 29 | Commit string `json:"commit"` 30 | } 31 | 32 | type ProjectScorecard struct { 33 | Timestamp uint64 `json:"timestamp"` 34 | Score float32 `json:"score"` 35 | Version string `json:"version"` 36 | Repo ProjectScorecardRepo `json:"repo"` 37 | Checks map[string]ProjectScorecardCheck `json:"checks"` 38 | } 39 | -------------------------------------------------------------------------------- /services/pkg/common/route/route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import "fmt" 4 | 5 | type routeHandler struct { 6 | pathPattern string 7 | } 8 | 9 | type routeMatch struct { 10 | is_match bool 11 | labels map[string]string 12 | } 13 | 14 | func NewRouteHandler(pathP string) (*routeHandler, error) { 15 | return nil, fmt.Errorf("unimplemented") 16 | } 17 | 18 | func (h *routeHandler) Match(path string) routeMatch { 19 | return routeMatch{} 20 | } 21 | 22 | func (r *routeMatch) IsMatch() bool { 23 | return r.is_match 24 | } 25 | 26 | func (r *routeMatch) Labels() map[string]string { 27 | return r.labels 28 | } 29 | -------------------------------------------------------------------------------- /services/pkg/common/route/route_test.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRoute(t *testing.T) { 10 | cases := []struct { 11 | name string 12 | pattern, path string 13 | is_match bool 14 | labels map[string]string 15 | }{ 16 | { 17 | "Basic positive case", 18 | "/a/:something/b", "/a/b/b", 19 | true, map[string]string{"something": "b"}, 20 | }, 21 | } 22 | 23 | for _, test := range cases { 24 | t.Run(test.name, func(t *testing.T) { 25 | handler, err := NewRouteHandler(test.pattern) 26 | 27 | assert.Nil(t, err) 28 | assert.NotNil(t, handler) 29 | 30 | m := handler.Match(test.path) 31 | assert.Equal(t, test.is_match, m.IsMatch()) 32 | assert.ElementsMatch(t, test.labels, m.Labels()) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /services/pkg/common/utils/strings.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/mitchellh/mapstructure" 9 | ) 10 | 11 | // Serialize an interface using JSON or return error string 12 | func Introspect(v interface{}) string { 13 | bytes, err := json.MarshalIndent(v, "", " ") 14 | if err != nil { 15 | return fmt.Sprintf("Error: %s", err.Error()) 16 | } else { 17 | return string(bytes) 18 | } 19 | } 20 | 21 | func CleanPath(path string) string { 22 | return filepath.Clean(path) 23 | } 24 | 25 | func MapStruct[T any](source interface{}, dest *T) error { 26 | return mapstructure.Decode(source, dest) 27 | } 28 | 29 | func SafelyGetValue[T any](target *T) T { 30 | var vi T 31 | if target != nil { 32 | vi = *target 33 | } 34 | 35 | return vi 36 | } 37 | 38 | func IsEmptyString(s string) bool { 39 | return s == "" 40 | } 41 | -------------------------------------------------------------------------------- /services/pkg/common/utils/tls.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | func TlsConfigFromEnvironment(serverName string) (tls.Config, error) { 11 | caCert, err := ioutil.ReadFile(os.Getenv("SERVICE_TLS_ROOT_CA")) 12 | if err != nil { 13 | return tls.Config{}, err 14 | } 15 | 16 | caCertPool := x509.NewCertPool() 17 | caCertPool.AppendCertsFromPEM(caCert) 18 | 19 | cert, err := tls.LoadX509KeyPair(os.Getenv("SERVICE_TLS_CERT"), os.Getenv("SERVICE_TLS_KEY")) 20 | if err != nil { 21 | return tls.Config{}, err 22 | } 23 | 24 | return tls.Config{ 25 | ServerName: serverName, 26 | Certificates: []tls.Certificate{cert}, 27 | RootCAs: caCertPool, 28 | MinVersion: tls.VersionTLS12, 29 | }, nil 30 | } 31 | -------------------------------------------------------------------------------- /services/pkg/common/utils/uid.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/oklog/ulid/v2" 8 | ) 9 | 10 | func NewUniqueId() string { 11 | t := time.Now() 12 | entropy := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0) 13 | return ulid.MustNew(ulid.Timestamp(t), entropy).String() 14 | } 15 | -------------------------------------------------------------------------------- /services/pkg/dcs/data_service.go: -------------------------------------------------------------------------------- 1 | package dcs 2 | 3 | import ( 4 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db" 5 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/messaging" 6 | ) 7 | 8 | type DataCollectionService struct { 9 | messagingService messaging.MessagingService 10 | vulnerabilityRepository *db.VulnerabilityRepository 11 | } 12 | 13 | func NewDataCollectionService(msgService messaging.MessagingService, 14 | vRepo *db.VulnerabilityRepository) (*DataCollectionService, error) { 15 | 16 | return &DataCollectionService{messagingService: msgService, 17 | vulnerabilityRepository: vRepo}, nil 18 | } 19 | 20 | func (svc *DataCollectionService) Start() { 21 | registerSubscriber(svc.messagingService, sbomCollectorSubscription()) 22 | registerSubscriber(svc.messagingService, vulnCollectorSubscription(svc.vulnerabilityRepository)) 23 | 24 | waitForSubscribers() 25 | } 26 | -------------------------------------------------------------------------------- /services/pkg/dcs/dispatcher.go: -------------------------------------------------------------------------------- 1 | package dcs 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 7 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/messaging" 8 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 10 | ) 11 | 12 | type eventSubscriptionHandler[T any] func(common_models.DomainEvent[T]) error 13 | 14 | type eventSubscription[T any] struct { 15 | name string 16 | topic, group string 17 | handler eventSubscriptionHandler[T] 18 | } 19 | 20 | var dispatcherWg sync.WaitGroup 21 | 22 | // Register a subscriber to the messaging service and increment 23 | // wait group. Perform generic event to subscriber specific type 24 | // conversion and invoke subscriber business logic 25 | func registerSubscriber[T any](msgService messaging.MessagingService, 26 | subscriber eventSubscription[T]) (messaging.MessagingQueueSubscription, error) { 27 | 28 | logger.Infof("Registering disaptcher name:%s topic:%s group:%s", 29 | subscriber.name, subscriber.topic, subscriber.group) 30 | 31 | sub, err := msgService.QueueSubscribe(subscriber.topic, subscriber.group, func(msg interface{}) { 32 | var event common_models.DomainEvent[T] 33 | if err := utils.MapStruct(msg, &event); err == nil { 34 | subscriber.handler(event) 35 | } else { 36 | logger.Infof("Error creating a domain event of type T from event msg: %v", err) 37 | } 38 | }) 39 | 40 | if err != nil { 41 | logger.Errorf("Error registering queue subscriber: %v", err) 42 | } 43 | 44 | dispatcherWg.Add(1) 45 | return sub, err 46 | } 47 | 48 | func waitForSubscribers() { 49 | logger.Infof("Dispatcher waiting for queue subscriptions to close") 50 | dispatcherWg.Wait() 51 | } 52 | -------------------------------------------------------------------------------- /services/pkg/dcs/sbom.go: -------------------------------------------------------------------------------- 1 | package dcs 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 7 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 8 | ) 9 | 10 | const ( 11 | sbomCollectorGroupName = "sbom-collector-group" 12 | sbomCollectorName = "SBOM Data Collector" 13 | ) 14 | 15 | type sbomCollector struct{} 16 | 17 | func sbomCollectorSubscription() eventSubscription[common_models.Artefact] { 18 | h := &sbomCollector{} 19 | return h.subscription() 20 | } 21 | 22 | func (s *sbomCollector) subscription() eventSubscription[common_models.Artefact] { 23 | cfg := config.TapServiceConfig() 24 | 25 | return eventSubscription[common_models.Artefact]{ 26 | name: sbomCollectorName, 27 | group: sbomCollectorGroupName, 28 | topic: cfg.GetPublisherConfig().GetTopicNames().GetUpstreamRequest(), 29 | handler: s.handler(), 30 | } 31 | } 32 | 33 | func (s *sbomCollector) handler() eventSubscriptionHandler[common_models.Artefact] { 34 | return func(event common_models.DomainEvent[common_models.Artefact]) error { 35 | return s.handle(event) 36 | } 37 | } 38 | 39 | func (s *sbomCollector) handle(event common_models.DomainEvent[common_models.Artefact]) error { 40 | log.Printf("SBOM collector - Handling artefact: %v", event.Data) 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /services/pkg/dcs/vulnerability.go: -------------------------------------------------------------------------------- 1 | package dcs 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | 7 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db" 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db/models" 10 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 11 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/openssf" 12 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 13 | ) 14 | 15 | const ( 16 | vulnCollectorGroupName = "vuln-collector-group" 17 | vulnCollectorName = "Vulnerability Data Collector" 18 | ) 19 | 20 | type vulnCollector struct { 21 | osvAdapter *openssf.OsvServiceAdapter 22 | repository *db.VulnerabilityRepository 23 | } 24 | 25 | func vulnCollectorSubscription(repository *db.VulnerabilityRepository) eventSubscription[common_models.Artefact] { 26 | osvAdapter := openssf.NewOsvServiceAdapter(openssf.DefaultServiceAdapterConfig()) 27 | 28 | h := vulnCollector{osvAdapter: osvAdapter, repository: repository} 29 | return h.subscription() 30 | } 31 | 32 | func (v *vulnCollector) subscription() eventSubscription[common_models.Artefact] { 33 | cfg := config.TapServiceConfig() 34 | 35 | return eventSubscription[common_models.Artefact]{ 36 | name: vulnCollectorName, 37 | group: vulnCollectorGroupName, 38 | topic: cfg.GetPublisherConfig().GetTopicNames().GetUpstreamRequest(), 39 | handler: v.handler(), 40 | } 41 | } 42 | 43 | func (v *vulnCollector) handler() eventSubscriptionHandler[common_models.Artefact] { 44 | return func(event common_models.DomainEvent[common_models.Artefact]) error { 45 | return v.handle(event) 46 | } 47 | } 48 | 49 | func (v *vulnCollector) handle(event common_models.DomainEvent[common_models.Artefact]) error { 50 | log.Printf("Vulnerability collector - Handling artefact: %v", event.Data) 51 | 52 | if config.IsFeatureDisabled("app_dcs_vulnerability_collector") { 53 | log.Printf("PDS Vulnerability collector is disabled with feature flag") 54 | return nil 55 | } 56 | 57 | vulnerabilities, err := v.osvAdapter.QueryPackage(event.Data.OpenSsfEcosystem(), 58 | event.Data.OpenSsfPackageName(), event.Data.OpenSsfPackageVersion()) 59 | if err != nil { 60 | log.Printf("Failed to fetch vulnerability from OSV adapter: %v", err) 61 | return err 62 | } 63 | 64 | vulns := utils.SafelyGetValue(vulnerabilities.Vulns) 65 | 66 | log.Printf("Fetched %d vulnerabilities for %s/%s", len(vulns), 67 | event.Data.OpenSsfEcosystem(), event.Data.OpenSsfPackageName()) 68 | 69 | for _, entry := range vulns { 70 | dataBytes, err := json.Marshal(entry) 71 | if err != nil { 72 | log.Printf("Failed to serialize vulnerability entry to JSON") 73 | continue 74 | } 75 | 76 | err = v.repository.Upsert(models.Vulnerability{ 77 | Ecosystem: event.Data.OpenSsfEcosystem(), 78 | Group: event.Data.Group, 79 | Name: event.Data.Name, 80 | SchemaType: models.VulnerabilitySchemaTypeOpenSSF, 81 | SchemaVersion: utils.SafelyGetValue(entry.SchemaVersion), 82 | ExternalId: utils.SafelyGetValue(entry.Id), 83 | ExternalSource: models.VulnerabilitySourceOpenSSF, 84 | Title: utils.SafelyGetValue(entry.Summary), 85 | Description: utils.SafelyGetValue(entry.Details), 86 | DataModifiedAt: utils.SafelyGetValue(entry.Modified), 87 | DataPublishedAt: utils.SafelyGetValue(entry.Published), 88 | Data: dataBytes, 89 | }) 90 | 91 | if err != nil { 92 | log.Printf("Failed to upsert vulnerability into repository") 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /services/pkg/pdp/auth.go: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/auth" 9 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 10 | envoy_api_v3_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 11 | envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" 12 | typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" 13 | "github.com/golang/protobuf/ptypes/wrappers" 14 | "google.golang.org/genproto/googleapis/rpc/code" 15 | "google.golang.org/genproto/googleapis/rpc/status" 16 | ) 17 | 18 | func (s *authorizationService) authenticateForUpstream(ctx context.Context, 19 | upstream common_models.ArtefactUpStream, 20 | req *envoy_service_auth_v3.AttributeContext_HttpRequest) (auth.AuthenticatedIdentity, error) { 21 | if !upstream.NeedAuthentication() { 22 | return auth.AnonymousIdentity(), nil 23 | } 24 | 25 | if req.Method == "HEAD" { 26 | return auth.AnonymousIdentity(), nil 27 | } 28 | 29 | authService, err := s.authProvider.IngressAuthService(upstream) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | identity, err := authService.Authenticate(ctx, auth.NewEnvoyIngressAuthAdapter(req)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return identity, nil 40 | } 41 | 42 | func (s *authorizationService) authenticationChallenge(ctx context.Context, 43 | upstream common_models.ArtefactUpStream, 44 | req *envoy_service_auth_v3.AttributeContext_HttpRequest) (*envoy_service_auth_v3.CheckResponse, error) { 45 | 46 | authChallenge := fmt.Sprintf("Basic realm=\"Authentication for upstream %s at %s\"", 47 | upstream.Name, upstream.RoutingRule.Prefix) 48 | 49 | return &envoy_service_auth_v3.CheckResponse{ 50 | HttpResponse: &envoy_service_auth_v3.CheckResponse_DeniedResponse{ 51 | DeniedResponse: &envoy_service_auth_v3.DeniedHttpResponse{ 52 | Status: &typev3.HttpStatus{ 53 | Code: http.StatusUnauthorized, 54 | }, 55 | Headers: []*envoy_api_v3_core.HeaderValueOption{ 56 | { 57 | Append: &wrappers.BoolValue{Value: false}, 58 | Header: &envoy_api_v3_core.HeaderValue{ 59 | Key: "WWW-Authenticate", 60 | Value: authChallenge, 61 | }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | Status: &status.Status{ 67 | Code: int32(code.Code_UNAUTHENTICATED), 68 | }, 69 | }, nil 70 | } 71 | -------------------------------------------------------------------------------- /services/pkg/pdp/extended_context.go: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/auth" 9 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 11 | envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | const ( 16 | projectIdHeaderName = "X-SGW-Project-Id" 17 | projectEnvHeaderName = "X-SGW-Project-Env" 18 | projectLabelsHeaderName = "X-SGW-Project-Labels" 19 | ) 20 | 21 | type extendedContext struct { 22 | innerCtx context.Context 23 | envoyCheckRequest *envoy_service_auth_v3.CheckRequest 24 | logger *zap.SugaredLogger 25 | identity auth.AuthenticatedIdentity 26 | upstream common_models.ArtefactUpStream 27 | artefact common_models.Artefact 28 | } 29 | 30 | func ExtendContext(ctx context.Context) *extendedContext { 31 | return &extendedContext{innerCtx: ctx} 32 | } 33 | 34 | func (ctx *extendedContext) Deadline() (deadline time.Time, ok bool) { 35 | return ctx.innerCtx.Deadline() 36 | } 37 | 38 | func (ctx *extendedContext) Done() <-chan struct{} { 39 | return ctx.innerCtx.Done() 40 | } 41 | 42 | func (ctx *extendedContext) Err() error { 43 | return ctx.innerCtx.Err() 44 | } 45 | 46 | func (ctx *extendedContext) Value(key any) any { 47 | return ctx.innerCtx.Value(key) 48 | } 49 | 50 | func (ctx *extendedContext) WithEnvoyCheckRequest(r *envoy_service_auth_v3.CheckRequest) *extendedContext { 51 | ctx.envoyCheckRequest = r 52 | return ctx 53 | } 54 | 55 | func (ctx *extendedContext) WithLogger(l *zap.SugaredLogger) *extendedContext { 56 | ctx.logger = l 57 | return ctx 58 | } 59 | 60 | func (ctx *extendedContext) WithAuthIdentity(id auth.AuthenticatedIdentity) *extendedContext { 61 | ctx.identity = id 62 | return ctx 63 | } 64 | 65 | func (ctx *extendedContext) WithArtefact(artefact common_models.Artefact) *extendedContext { 66 | ctx.artefact = artefact 67 | return ctx 68 | } 69 | 70 | func (ctx *extendedContext) WithUpstream(upstream common_models.ArtefactUpStream) *extendedContext { 71 | ctx.upstream = upstream 72 | return ctx 73 | } 74 | 75 | // https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers 76 | func (ctx *extendedContext) RequestHost() string { 77 | return ctx.envoyCheckRequest.Attributes.Request.Http.Headers[":authority"] 78 | } 79 | 80 | func (ctx *extendedContext) RequestHeader(name string) string { 81 | return ctx.envoyCheckRequest.Attributes.Request.Http.Headers[":"+strings.ToLower(name)] 82 | } 83 | 84 | func (ctx *extendedContext) RequestPath() string { 85 | return ctx.envoyCheckRequest.Attributes.Request.Http.Path 86 | } 87 | 88 | func (ctx *extendedContext) ValidHost() bool { 89 | parts := strings.SplitN(ctx.RequestHost(), ".", 2) 90 | return len(parts) == 2 91 | } 92 | 93 | func (ctx *extendedContext) GatewayDomain() string { 94 | return strings.SplitN(ctx.RequestHost(), ".", 2)[0] 95 | } 96 | 97 | func (ctx *extendedContext) EnvironmentDomain() string { 98 | if ctx.ValidHost() { 99 | return strings.SplitN(ctx.RequestHost(), ".", 2)[1] 100 | } else { 101 | return "" 102 | } 103 | } 104 | 105 | func (ctx *extendedContext) UserId() string { 106 | if ctx.identity == nil { 107 | return "" 108 | } 109 | 110 | return ctx.identity.UserId() 111 | } 112 | 113 | func (ctx *extendedContext) OrgId() string { 114 | if ctx.identity == nil { 115 | return "" 116 | } 117 | 118 | return ctx.identity.OrgId() 119 | } 120 | 121 | // There is a bug here. ProjectId is overridden through env 122 | // but policy engine still sees the projectId encoded in username 123 | func (ctx *extendedContext) ProjectId() string { 124 | projectId := ctx.RequestHeader(projectIdHeaderName) 125 | if utils.IsEmptyString(projectId) && (ctx.identity != nil) { 126 | projectId = ctx.identity.ProjectId() 127 | } 128 | 129 | return projectId 130 | } 131 | 132 | func (ctx *extendedContext) Artefact() common_models.Artefact { 133 | return ctx.artefact 134 | } 135 | 136 | func (ctx *extendedContext) Upstream() common_models.ArtefactUpStream { 137 | return ctx.upstream 138 | } 139 | -------------------------------------------------------------------------------- /services/pkg/pdp/pds_client.go: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 8 | pds_api "github.com/abhisek/supply-chain-gateway/services/gen" 9 | raya_api "github.com/abhisek/supply-chain-gateway/services/gen" 10 | common_adapters "github.com/abhisek/supply-chain-gateway/services/pkg/common/adapters" 11 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 12 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/openssf" 13 | 14 | "google.golang.org/grpc" 15 | ) 16 | 17 | const ( 18 | pdsClientTypeLocal = "local" 19 | pdsClientTypeRaya = "raya" 20 | ) 21 | 22 | type PolicyDataServiceResponse struct { 23 | Vulnerabilities []common_models.ArtefactVulnerability `json:"vulnerabilities"` 24 | Licenses []common_models.ArtefactLicense `json:"licenses"` 25 | Scorecard openssf.ProjectScorecard 26 | } 27 | 28 | type PolicyDataClientInterface interface { 29 | GetPackageMetaByVersion(ctx context.Context, ecosystem, group, name, version string) (PolicyDataServiceResponse, error) 30 | } 31 | 32 | func NewPolicyDataServiceClient(cfg *config_api.PdsClientConfig) (PolicyDataClientInterface, error) { 33 | grpconn, err := buildGrpcClient(cfg.GetCommon().GetHost(), 34 | fmt.Sprint(cfg.GetCommon().GetPort()), cfg.GetCommon().GetMtls()) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | switch cfg.Type { 40 | case config_api.PdsClientType_LOCAL: 41 | return NewLocalPolicyDataClient(grpconn), nil 42 | case config_api.PdsClientType_RAYA: 43 | return NewRayaPolicyDataServiceClient(grpconn), nil 44 | default: 45 | return nil, fmt.Errorf("unknown pds client type:%s", cfg.Type.String()) 46 | } 47 | } 48 | 49 | func buildGrpcClient(host string, port string, mtls bool) (*grpc.ClientConn, error) { 50 | if mtls { 51 | return common_adapters.GrpcMtlsClient("pds_secure_client", host, host, port, 52 | common_adapters.NoGrpcDialOptions, common_adapters.NoGrpcConfigurer) 53 | } else { 54 | return common_adapters.GrpcInsecureClient("pds_insecure_client", host, port, 55 | common_adapters.NoGrpcDialOptions, common_adapters.NoGrpcConfigurer) 56 | } 57 | } 58 | 59 | func NewLocalPolicyDataClient(cc grpc.ClientConnInterface) PolicyDataClientInterface { 60 | return &pdsLocalImplementation{ 61 | client: pds_api.NewPolicyDataServiceClient(cc), 62 | } 63 | } 64 | 65 | func NewRayaPolicyDataServiceClient(conn grpc.ClientConnInterface) PolicyDataClientInterface { 66 | return &pdsRayaClient{ 67 | client: raya_api.NewRayaClient(conn), 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /services/pkg/pdp/pds_client_local.go: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | import ( 4 | "context" 5 | 6 | pds_api "github.com/abhisek/supply-chain-gateway/services/gen" 7 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 8 | ) 9 | 10 | type pdsLocalImplementation struct { 11 | client pds_api.PolicyDataServiceClient 12 | } 13 | 14 | func (pds *pdsLocalImplementation) GetPackageMetaByVersion(ctx context.Context, 15 | ecosystem, group, name, version string) (PolicyDataServiceResponse, error) { 16 | resp, err := pds.client.FindVulnerabilitiesByArtefact(ctx, &pds_api.FindVulnerabilityByArtefactRequest{ 17 | Artefact: &pds_api.Artefact{ 18 | Ecosystem: ecosystem, 19 | Group: group, 20 | Name: name, 21 | Version: version, 22 | }, 23 | }) 24 | 25 | if err != nil { 26 | return PolicyDataServiceResponse{}, err 27 | } 28 | 29 | return PolicyDataServiceResponse{ 30 | Vulnerabilities: pds.mapVulnerabilities(resp.Vulnerabilities), 31 | }, nil 32 | } 33 | 34 | func (pds *pdsLocalImplementation) mapVulnerabilities(src []*pds_api.VulnerabilityMeta) []common_models.ArtefactVulnerability { 35 | target := []common_models.ArtefactVulnerability{} 36 | 37 | if src == nil || len(src) == 0 { 38 | return target 39 | } 40 | 41 | for _, s := range src { 42 | mv := common_models.ArtefactVulnerability{ 43 | Name: s.Title, 44 | Id: common_models.ArtefactVulnerabilityId{ 45 | Source: s.Source, 46 | Id: s.Id, 47 | }, 48 | Scores: []common_models.ArtefactVulnerabilityScore{}, 49 | } 50 | 51 | switch s.Severity { 52 | case pds_api.VulnerabilitySeverity_CRITICAL: 53 | mv.Severity = common_models.ArtefactVulnerabilitySeverityCritical 54 | break 55 | case pds_api.VulnerabilitySeverity_HIGH: 56 | mv.Severity = common_models.ArtefactVulnerabilitySeverityHigh 57 | break 58 | case pds_api.VulnerabilitySeverity_MEDIUM: 59 | mv.Severity = common_models.ArtefactVulnerabilitySeverityMedium 60 | break 61 | case pds_api.VulnerabilitySeverity_LOW: 62 | mv.Severity = common_models.ArtefactVulnerabilitySeverityLow 63 | default: 64 | mv.Severity = common_models.ArtefactVulnerabilitySeverityInfo 65 | } 66 | 67 | for _, score := range s.Scores { 68 | mv.Scores = append(mv.Scores, common_models.ArtefactVulnerabilityScore{ 69 | Type: score.Type, 70 | Value: score.Value, 71 | }) 72 | } 73 | 74 | target = append(target, mv) 75 | } 76 | 77 | return target 78 | } 79 | -------------------------------------------------------------------------------- /services/pkg/pdp/pds_client_raya.go: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | "strings" 9 | 10 | raya_api "github.com/abhisek/supply-chain-gateway/services/gen" 11 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 12 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/openssf" 13 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 14 | ) 15 | 16 | type pdsRayaClient struct { 17 | client raya_api.RayaClient 18 | } 19 | 20 | func (pds *pdsRayaClient) GetPackageMetaByVersion(ctx context.Context, 21 | ecosystem, group, name, version string) (PolicyDataServiceResponse, error) { 22 | pkgName := "" 23 | 24 | if !utils.IsEmptyString(group) && ecosystem == openssf.VulnerabilityEcosystemMaven { 25 | pkgName = fmt.Sprintf("%s:%s", group, name) 26 | } else { 27 | pkgName = name 28 | } 29 | 30 | request := &raya_api.PackageVersionMetaQueryRequest{ 31 | PackageVersion: &raya_api.PackageVersion{ 32 | Package: &raya_api.Package{ 33 | Ecosystem: rayaEcosystemName(ecosystem), 34 | Name: pkgName, 35 | }, 36 | Version: version, 37 | }, 38 | } 39 | 40 | log.Printf("Querying Raya with: %v", request) 41 | 42 | response, err := pds.client.GetPackageMetaByVersion(ctx, request) 43 | if err != nil { 44 | return PolicyDataServiceResponse{}, err 45 | } 46 | 47 | severityMapper := func(s raya_api.Severity) string { 48 | switch s { 49 | case raya_api.Severity_CRITICAL: 50 | return common_models.ArtefactVulnerabilitySeverityCritical 51 | case raya_api.Severity_HIGH: 52 | return common_models.ArtefactVulnerabilitySeverityHigh 53 | case raya_api.Severity_MEDIUM: 54 | return common_models.ArtefactVulnerabilitySeverityMedium 55 | case raya_api.Severity_LOW: 56 | return common_models.ArtefactVulnerabilitySeverityLow 57 | default: 58 | return common_models.ArtefactVulnerabilitySeverityInfo 59 | } 60 | } 61 | 62 | pdsResponse := PolicyDataServiceResponse{} 63 | for _, adv := range response.Advisories { 64 | if adv == nil { 65 | continue 66 | } 67 | 68 | pdsResponse.Vulnerabilities = append(pdsResponse.Vulnerabilities, common_models.ArtefactVulnerability{ 69 | Id: common_models.ArtefactVulnerabilityId{ 70 | Source: adv.Source, 71 | Id: adv.SourceId, 72 | }, 73 | Name: adv.Title, 74 | Severity: severityMapper(adv.AdvisorySeverity.Severity), 75 | Scores: []common_models.ArtefactVulnerabilityScore{ 76 | { 77 | Type: common_models.ArtefactVulnerabilityScoreTypeCVSSv3, 78 | Value: strconv.FormatFloat(float64(adv.AdvisorySeverity.Cvssv3Score), 'f', -1, 32), 79 | }, 80 | }, 81 | }) 82 | } 83 | 84 | for _, license := range response.Licenses { 85 | pdsResponse.Licenses = append(pdsResponse.Licenses, common_models.ArtefactLicense{ 86 | Type: common_models.ArtefactLicenseTypeSpdx, 87 | Id: license, 88 | Name: license, 89 | }) 90 | } 91 | 92 | if response.ProjectScorecard != nil { 93 | pdsResponse.Scorecard.Timestamp = response.ProjectScorecard.Timestamp 94 | pdsResponse.Scorecard.Score = response.ProjectScorecard.Score 95 | pdsResponse.Scorecard.Version = response.ProjectScorecard.Version 96 | 97 | if response.ProjectScorecard.Repo != nil { 98 | pdsResponse.Scorecard.Repo.Name = response.ProjectScorecard.Repo.Name 99 | pdsResponse.Scorecard.Repo.Commit = response.ProjectScorecard.Repo.Commit 100 | } 101 | 102 | checksMap := map[string]openssf.ProjectScorecardCheck{} 103 | if response.ProjectScorecard.Checks != nil { 104 | checksMap[openssf.ScBinaryArtifactsCheck] = 105 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.BinaryArtifacts) 106 | checksMap[openssf.ScBranchProtectionCheck] = 107 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.BranchProtection) 108 | checksMap[openssf.ScCiiBestPracticeCheck] = 109 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.CiiBestPractices) 110 | checksMap[openssf.ScCodeReviewCheck] = 111 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.CodeReview) 112 | checksMap[openssf.ScDangerousWorkflowCheck] = 113 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.DangerousWorkflow) 114 | checksMap[openssf.ScDependencyUpdateToolCheck] = 115 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.DependencyUpdateTool) 116 | checksMap[openssf.ScFuzzingCheck] = 117 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.Fuzzing) 118 | checksMap[openssf.ScLicenseCheck] = 119 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.License) 120 | checksMap[openssf.ScMaintainedCheck] = 121 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.Maintained) 122 | checksMap[openssf.ScPackagingCheck] = 123 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.Packaging) 124 | checksMap[openssf.ScPinnedDependenciesCheck] = 125 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.PinnedDependencies) 126 | checksMap[openssf.ScSastCheck] = 127 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.Sast) 128 | checksMap[openssf.ScSecurityPolicyCheck] = 129 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.SecurityPolicy) 130 | checksMap[openssf.ScSignedReleasesCheck] = 131 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.SignedReleases) 132 | checksMap[openssf.ScTokenPermissionsCheck] = 133 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.TokenPermissions) 134 | checksMap[openssf.ScVulnerabilitiesCheck] = 135 | rayaScorecardCheckToOpenSsfScorecardCheck(response.ProjectScorecard.Checks.Vulnerabilities) 136 | } 137 | 138 | pdsResponse.Scorecard.Checks = checksMap 139 | } 140 | 141 | return pdsResponse, nil 142 | } 143 | 144 | func rayaScorecardCheckToOpenSsfScorecardCheck(sc *raya_api.ProjectScorecardCheck) openssf.ProjectScorecardCheck { 145 | psc := openssf.ProjectScorecardCheck{} 146 | if sc == nil { 147 | return psc 148 | } 149 | 150 | psc.Reason = sc.Reason 151 | psc.Score = sc.Score 152 | 153 | return psc 154 | } 155 | 156 | func rayaEcosystemName(name string) string { 157 | return strings.ToUpper(name) 158 | } 159 | -------------------------------------------------------------------------------- /services/pkg/pdp/policy_engine.go: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "time" 11 | 12 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 13 | "github.com/open-policy-agent/opa/rego" 14 | ) 15 | 16 | type PolicyEngine struct { 17 | lock sync.Mutex 18 | repository string 19 | rego *rego.Rego 20 | query *rego.PreparedEvalQuery 21 | } 22 | 23 | const ( 24 | policyQuery = "x = data.pdp" 25 | ) 26 | 27 | func NewPolicyEngine(path string, changeMonitor bool) (*PolicyEngine, error) { 28 | svc := PolicyEngine{repository: path} 29 | err := svc.Load(changeMonitor) 30 | if err != nil { 31 | return &PolicyEngine{}, err 32 | } 33 | 34 | return &svc, nil 35 | } 36 | 37 | func (svc *PolicyEngine) Evaluate(ctx context.Context, input PolicyInput) (PolicyResponse, error) { 38 | svc.lock.Lock() 39 | defer svc.lock.Unlock() 40 | 41 | rs, err := svc.query.Eval(ctx, rego.EvalInput(input)) 42 | if err != nil { 43 | return PolicyResponse{}, err 44 | } 45 | 46 | if len(rs) == 0 || rs[0].Bindings["x"] == nil { 47 | return PolicyResponse{}, errors.New("Policy evaluation returned unexpected result") 48 | } 49 | 50 | x := rs[0].Bindings["x"] 51 | var p PolicyResponse 52 | err = utils.MapStruct(x, &p) 53 | if err != nil { 54 | return PolicyResponse{}, err 55 | } 56 | 57 | return p, nil 58 | } 59 | 60 | func (svc *PolicyEngine) Load(changeMonitor bool) error { 61 | err := svc.loadPolicy() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | // TODO: Switch to inotify/kqueue 67 | if changeMonitor { 68 | d, err := time.ParseDuration(policyEvalChangeMonitorInterval) 69 | if err != nil { 70 | log.Printf("Failed to parse ticker duration for policy reload") 71 | return err 72 | } 73 | 74 | ticker := time.NewTicker(d) 75 | tickerStop := make(chan os.Signal) 76 | 77 | signal.Notify(tickerStop, os.Interrupt) 78 | go func() { 79 | for { 80 | select { 81 | case <-ticker.C: 82 | log.Printf("Re-loading policy from path: %s", svc.repository) 83 | err := svc.loadPolicy() 84 | if err != nil { 85 | log.Printf("Failed to reload policy: %s", err.Error()) 86 | } 87 | case <-tickerStop: 88 | ticker.Stop() 89 | return 90 | } 91 | } 92 | }() 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func (svc *PolicyEngine) loadPolicy() error { 99 | queryFn := rego.Query(policyQuery) 100 | policyDoc := rego.Load([]string{svc.repository}, nil) 101 | 102 | r := rego.New(queryFn, policyDoc) 103 | q, err := r.PrepareForEval(context.Background()) 104 | 105 | if err != nil { 106 | return err 107 | } 108 | 109 | svc.lock.Lock() 110 | defer svc.lock.Unlock() 111 | 112 | svc.rego = r 113 | svc.query = &q 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /services/pkg/pdp/policy_model.go: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | import ( 4 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 5 | ) 6 | 7 | const ( 8 | policyInputKind = "PolicyInput" 9 | policyInputMajorVersion = 1 10 | policyInputMinorVersion = 0 11 | policyInputPatchVersion = 0 12 | policyEvalChangeMonitorInterval = "5s" 13 | ) 14 | 15 | type PolicyEvalTargetArtefact struct { 16 | common_models.Artefact 17 | } 18 | 19 | type PolicyEvalTargetUpstream struct { 20 | common_models.ArtefactUpStream 21 | } 22 | 23 | type PolicyEvalTargetVulnerability struct { 24 | common_models.ArtefactVulnerability 25 | } 26 | 27 | type PolicyEvalTargetLicense struct { 28 | common_models.ArtefactLicense 29 | } 30 | 31 | type PolicyInputVersion struct { 32 | Major int8 `json:"major"` 33 | Minor int8 `json:"minor"` 34 | Patch int8 `json:"patch"` 35 | } 36 | 37 | type PolicyInputPrincipal struct { 38 | UserId string `json:"userId"` 39 | OrgId string `json:"orgId"` 40 | ProjectId string `json:"projectId"` 41 | } 42 | 43 | type PolicyInputTarget struct { 44 | Artefact PolicyEvalTargetArtefact `json:"artefact"` 45 | Upstream PolicyEvalTargetUpstream `json:"upstream"` 46 | Vulnerabilities []PolicyEvalTargetVulnerability `json:"vulnerabilities"` 47 | Licenses []PolicyEvalTargetLicense `json:"licenses"` 48 | } 49 | 50 | type PolicyInput struct { 51 | Kind string `json:"kind"` 52 | Version PolicyInputVersion `json:"version"` 53 | Target PolicyInputTarget `json:"target"` 54 | Principal PolicyInputPrincipal `json:"principal"` 55 | } 56 | 57 | type PolicyViolation struct { 58 | Code int64 `json:"code"` 59 | Message string `json:"message"` 60 | } 61 | 62 | type PolicyResponse struct { 63 | Allow bool `json:"allow"` 64 | Violations []PolicyViolation `json:"violations"` 65 | } 66 | -------------------------------------------------------------------------------- /services/pkg/pdp/policy_model_utils.go: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | import ( 4 | "github.com/abhisek/supply-chain-gateway/services/pkg/auth" 5 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 6 | ) 7 | 8 | func NewPolicyInput(target common_models.Artefact, 9 | upstream common_models.ArtefactUpStream, 10 | requester auth.AuthenticatedIdentity, 11 | enrichments PolicyDataServiceResponse) PolicyInput { 12 | 13 | vulns := []PolicyEvalTargetVulnerability{} 14 | for _, v := range enrichments.Vulnerabilities { 15 | vulns = append(vulns, PolicyEvalTargetVulnerability{v}) 16 | } 17 | 18 | lics := []PolicyEvalTargetLicense{} 19 | for _, l := range enrichments.Licenses { 20 | lics = append(lics, PolicyEvalTargetLicense{l}) 21 | } 22 | 23 | return PolicyInput{ 24 | Kind: policyInputKind, 25 | Version: PolicyInputVersion{ 26 | Major: policyInputMajorVersion, 27 | Minor: policyInputMinorVersion, 28 | Patch: policyInputPatchVersion, 29 | }, 30 | Target: PolicyInputTarget{ 31 | Artefact: PolicyEvalTargetArtefact{target}, 32 | Upstream: PolicyEvalTargetUpstream{upstream}, 33 | Vulnerabilities: vulns, 34 | Licenses: lics, 35 | }, 36 | Principal: PolicyInputPrincipal{ 37 | UserId: requester.UserId(), 38 | ProjectId: requester.ProjectId(), 39 | OrgId: requester.OrgId(), 40 | }, 41 | } 42 | } 43 | 44 | func (s PolicyResponse) Allowed() bool { 45 | return (s.Allow) && (len(s.Violations) == 0) 46 | } 47 | -------------------------------------------------------------------------------- /services/pkg/pdp/utils.go: -------------------------------------------------------------------------------- 1 | package pdp 2 | 3 | import ( 4 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 5 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 6 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 7 | ) 8 | 9 | // Stop gap method to map a spec based upstream into legacy upstream 10 | func toLegacyUpstream(us *config_api.GatewayUpstream) common_models.ArtefactUpStream { 11 | return common_models.ToUpstream(us) 12 | } 13 | 14 | func isMonitorMode() bool { 15 | return config.PdpServiceConfig().MonitorMode 16 | } 17 | -------------------------------------------------------------------------------- /services/pkg/pds/openssf_vuln_wrap.go: -------------------------------------------------------------------------------- 1 | package pds 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | api "github.com/abhisek/supply-chain-gateway/services/gen" 8 | 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db/models" 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/openssf" 11 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 12 | ) 13 | 14 | type openssfVulnWrapper struct { 15 | model models.Vulnerability 16 | openssfVulnerability openssf.OsvVulnerability 17 | } 18 | 19 | func init() { 20 | registerVulnerabilitySchemaWrapper(vulnerabilitySchemaWrapperRegistration{ 21 | CanHandle: func(t, v string) bool { 22 | return (t == models.VulnerabilitySchemaTypeOpenSSF) 23 | }, 24 | Handle: func(v models.Vulnerability) (VulnerabilitySchemaWrapper, error) { 25 | w := &openssfVulnWrapper{model: v} 26 | err := json.Unmarshal(v.Data, &w.openssfVulnerability) 27 | 28 | return w, err 29 | }, 30 | }) 31 | } 32 | 33 | func (w *openssfVulnWrapper) CVE() string { 34 | aliases := utils.SafelyGetValue(w.openssfVulnerability.Aliases) 35 | for _, alias := range aliases { 36 | if strings.HasPrefix(alias, "CVE-") { 37 | return alias 38 | } 39 | } 40 | 41 | return "" 42 | } 43 | 44 | func (w *openssfVulnWrapper) References() []*api.VulnerabilityReference { 45 | refs := utils.SafelyGetValue(w.openssfVulnerability.References) 46 | vRefs := []*api.VulnerabilityReference{} 47 | 48 | for _, i := range refs { 49 | vRefs = append(vRefs, &api.VulnerabilityReference{ 50 | Type: string(utils.SafelyGetValue(i.Type)), 51 | Url: utils.SafelyGetValue(i.Url), 52 | }) 53 | } 54 | 55 | return vRefs 56 | } 57 | 58 | func (w *openssfVulnWrapper) CWEs() []string { 59 | cwes := w.databaseSpecific()["cwe_ids"] 60 | if cs, ok := cwes.([]string); ok { 61 | return cs 62 | } else { 63 | return []string{} 64 | } 65 | } 66 | 67 | func (w *openssfVulnWrapper) Affects() []*vulnerabilityAffected { 68 | av := []*vulnerabilityAffected{} 69 | 70 | osvAffected := utils.SafelyGetValue(w.openssfVulnerability.Affected) 71 | for _, osvA := range osvAffected { 72 | av = append(av, &vulnerabilityAffected{ 73 | Versions: utils.SafelyGetValue(osvA.Versions), 74 | }) 75 | } 76 | 77 | return av 78 | } 79 | 80 | func (w *openssfVulnWrapper) FriendlySeverity() string { 81 | s := w.databaseSpecific()["severity"] 82 | if cs, ok := s.(string); ok { 83 | return cs 84 | } else { 85 | return "" 86 | } 87 | } 88 | 89 | func (w *openssfVulnWrapper) FriendlySeverityCode() api.VulnerabilitySeverity { 90 | switch w.FriendlySeverity() { 91 | case "CRITICAL": 92 | return api.VulnerabilitySeverity_CRITICAL 93 | case "HIGH": 94 | return api.VulnerabilitySeverity_HIGH 95 | case "MEDIUM": 96 | return api.VulnerabilitySeverity_HIGH 97 | case "LOW": 98 | return api.VulnerabilitySeverity_LOW 99 | case "INFO": 100 | return api.VulnerabilitySeverity_INFO 101 | default: 102 | return api.VulnerabilitySeverity_UNKNOWN_SEVERITY 103 | } 104 | } 105 | 106 | func (w *openssfVulnWrapper) Severity() []*api.VulnerabilityScore { 107 | severity := []*api.VulnerabilityScore{} 108 | s := utils.SafelyGetValue(w.openssfVulnerability.Severity) 109 | 110 | for _, osvSev := range s { 111 | severity = append(severity, &api.VulnerabilityScore{ 112 | Type: string(utils.SafelyGetValue(osvSev.Type)), 113 | Value: utils.SafelyGetValue(osvSev.Score), 114 | }) 115 | } 116 | 117 | return severity 118 | } 119 | 120 | func (w *openssfVulnWrapper) databaseSpecific() map[string]interface{} { 121 | return utils.SafelyGetValue(w.openssfVulnerability.DatabaseSpecific) 122 | } 123 | -------------------------------------------------------------------------------- /services/pkg/pds/policy_data_service.go: -------------------------------------------------------------------------------- 1 | package pds 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | 8 | api "github.com/abhisek/supply-chain-gateway/services/gen" 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db" 10 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | type policyDataServer struct { 16 | api.PolicyDataServiceServer 17 | repository *db.VulnerabilityRepository 18 | } 19 | 20 | func NewPolicyDataService(repo *db.VulnerabilityRepository) (api.PolicyDataServiceServer, error) { 21 | return &policyDataServer{ 22 | repository: repo, 23 | }, nil 24 | } 25 | 26 | func (s *policyDataServer) FindVulnerabilitiesByArtefact(ctx context.Context, 27 | req *api.FindVulnerabilityByArtefactRequest) (*api.VulnerabilityList, error) { 28 | 29 | vulnList := &api.VulnerabilityList{ 30 | Vulnerabilities: []*api.VulnerabilityMeta{}, 31 | } 32 | 33 | log.Printf("Handling query req for: %s/%s", req.Artefact.Ecosystem, req.Artefact.Name) 34 | 35 | // Lookup all vulnerabilities by Ecosystem, group, name 36 | dbVulnerabilities, err := s.repository.Lookup(req.Artefact.Ecosystem, req.Artefact.Group, 37 | req.Artefact.Name) 38 | if err != nil { 39 | return vulnList, status.Errorf(codes.Internal, "failed on query: %v", err) 40 | } 41 | 42 | // Fuzzy match to find vulnerabilities applicable for the version 43 | for _, dbVuln := range dbVulnerabilities { 44 | wrappedVuln, err := wrapVuln(dbVuln) 45 | if err != nil { 46 | log.Printf("failed to wrap db vuln: %v", err) 47 | continue 48 | } 49 | 50 | if s.match(wrappedVuln, req.Artefact) { 51 | vulnList.Vulnerabilities = append(vulnList.Vulnerabilities, &api.VulnerabilityMeta{ 52 | Id: dbVuln.ExternalId, 53 | Source: dbVuln.ExternalSource, 54 | Title: dbVuln.Title, 55 | Severity: wrappedVuln.FriendlySeverityCode(), 56 | Scores: wrappedVuln.Severity(), 57 | }) 58 | } 59 | } 60 | 61 | log.Printf("Supplying vulnerabilities: %s", utils.Introspect(vulnList)) 62 | return vulnList, nil 63 | } 64 | 65 | func (s *policyDataServer) GetVulnerabilityDetails(ctx context.Context, 66 | req *api.GetVulnerabilityByIdRequest) (*api.VulnerabilityDetail, error) { 67 | return &api.VulnerabilityDetail{}, errors.New("unimplemented endpoint") 68 | } 69 | 70 | // TODO: Fuzzy match 71 | func (s *policyDataServer) match(vuln VulnerabilitySchemaWrapper, artefact *api.Artefact) bool { 72 | affects := vuln.Affects() 73 | for _, a := range affects { 74 | for _, v := range a.Versions { 75 | if artefact.Version == v { 76 | return true 77 | } 78 | } 79 | } 80 | 81 | return false 82 | } 83 | -------------------------------------------------------------------------------- /services/pkg/pds/vulnerability_model_wrap.go: -------------------------------------------------------------------------------- 1 | package pds 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | api "github.com/abhisek/supply-chain-gateway/services/gen" 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/db/models" 9 | ) 10 | 11 | type vulnerabilityAffected struct { 12 | Versions []string 13 | } 14 | 15 | // We may have different vulnerability sources and data provider. 16 | // The vulnerability data has meaning based on vuln.SchemaType and vuln.SchemaVersion 17 | // We need adapters/wrappers that can be plugged in to make sense of vuln.Data 18 | // and provide an uniform interface to the rest of the system 19 | type VulnerabilitySchemaWrapper interface { 20 | References() []*api.VulnerabilityReference 21 | CVE() string 22 | CWEs() []string 23 | 24 | Affects() []*vulnerabilityAffected 25 | 26 | // Return on of CRITICAL, HIGH, MEDIUM, LOW 27 | FriendlySeverity() string 28 | FriendlySeverityCode() api.VulnerabilitySeverity 29 | 30 | // Standard form 31 | Severity() []*api.VulnerabilityScore 32 | } 33 | 34 | // Provide a way for different schema wrappers to register themselves 35 | type vulnerabilitySchemaWrapperRegistration struct { 36 | CanHandle func(t, v string) bool 37 | Handle func(models.Vulnerability) (VulnerabilitySchemaWrapper, error) 38 | } 39 | 40 | var availableVulnerabilitySchemaWrappers []vulnerabilitySchemaWrapperRegistration 41 | 42 | func registerVulnerabilitySchemaWrapper(r vulnerabilitySchemaWrapperRegistration) { 43 | availableVulnerabilitySchemaWrappers = append(availableVulnerabilitySchemaWrappers, r) 44 | } 45 | 46 | func wrapVuln(m models.Vulnerability) (VulnerabilitySchemaWrapper, error) { 47 | for _, w := range availableVulnerabilitySchemaWrappers { 48 | if w.CanHandle(m.SchemaType, m.SchemaVersion) { 49 | h, err := w.Handle(m) 50 | if err != nil { 51 | log.Printf("Handler failed to setup for %v with error %v", w, err) 52 | continue 53 | } 54 | 55 | return h, nil 56 | } 57 | } 58 | 59 | return nil, fmt.Errorf("no wrappers found for %s:%s", m.SchemaType, m.SchemaVersion) 60 | } 61 | -------------------------------------------------------------------------------- /services/pkg/secrets/provider.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | type SecretProvider interface { 4 | GetSecret(string) (string, error) 5 | } 6 | -------------------------------------------------------------------------------- /services/pkg/secrets/provider_env.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/utils" 8 | ) 9 | 10 | type envSecretProvider struct{} 11 | 12 | func NewEnvSecretProvider() (SecretProvider, error) { 13 | return &envSecretProvider{}, nil 14 | } 15 | 16 | func (p *envSecretProvider) GetSecret(name string) (string, error) { 17 | v, b := os.LookupEnv(name) 18 | if !b || utils.IsEmptyString(v) { 19 | return "", fmt.Errorf("secret %s returned empty or non-existent", name) 20 | } 21 | 22 | return v, nil 23 | } 24 | -------------------------------------------------------------------------------- /services/pkg/secrets/secret.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | config_api "github.com/abhisek/supply-chain-gateway/services/gen" 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 9 | ) 10 | 11 | const ( 12 | SecretProviderEnv = "environment" 13 | SecretProviderAwsSecretsManager = "aws-secrets-manager" 14 | SecretProviderHashicorpVault = "hashicorp-vault" 15 | ) 16 | 17 | /** 18 | Possible source of secret 19 | 20 | Environment 21 | Vault 22 | AWS Secrets Manager 23 | 24 | Some of these secrets provider need authentication of their own. The adapter need special 25 | environmental config to be able to fetch secret 26 | */ 27 | 28 | func init() { 29 | initCache() 30 | } 31 | 32 | func initCache() { 33 | log.Printf("Initializing secrets cache") 34 | } 35 | 36 | func getSecret(key string, evictCache bool) (string, error) { 37 | s, err := config.GetSecret(key) 38 | if err != nil { 39 | return "", err 40 | } 41 | 42 | if evictCache { 43 | // TODO: Evict the cache 44 | } 45 | 46 | // TODO: Lookup cache 47 | 48 | a, err := resolveProvider(s.Source) 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | // FIXME: Refactor the secret provider interface 54 | return a.GetSecret(s.GetEnvironment().Key) 55 | } 56 | 57 | func resolveProvider(src config_api.GatewaySecretSource) (SecretProvider, error) { 58 | switch src { 59 | case config_api.GatewaySecretSource_Environment: 60 | return NewEnvSecretProvider() 61 | default: 62 | return nil, fmt.Errorf("provider not found for %s", src.String()) 63 | } 64 | } 65 | 66 | func GetSecret(key string) (string, error) { 67 | return getSecret(key, false) 68 | } 69 | -------------------------------------------------------------------------------- /services/pkg/tap/tap_handler_event_pub.go: -------------------------------------------------------------------------------- 1 | package tap 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/config" 9 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/messaging" 10 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 11 | 12 | envoy_v3_ext_proc_pb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" 13 | ) 14 | 15 | type tapEventPublisher struct { 16 | messagingService messaging.MessagingService 17 | } 18 | 19 | func NewTapEventPublisherRegistration(msgService messaging.MessagingService) TapHandlerRegistration { 20 | return TapHandlerRegistration{ 21 | ContinueOnError: true, 22 | Handler: &tapEventPublisher{messagingService: msgService}, 23 | } 24 | } 25 | 26 | func (h *tapEventPublisher) HandleRequestHeaders(ctx context.Context, 27 | req *envoy_v3_ext_proc_pb.ProcessingRequest_RequestHeaders) error { 28 | 29 | cfg := config.TapServiceConfig() 30 | 31 | log.Printf("Publishing request headers event") 32 | 33 | host, path, err := findHostAndPath(req) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | artefact, err := common_models.GetArtefactByHostAndPath(host, path) 39 | if err != nil { 40 | return fmt.Errorf("Failed to resolve artefact") 41 | } 42 | 43 | topic := cfg.GetPublisherConfig().GetTopicNames().GetUpstreamRequest() 44 | 45 | // TODO: Migrate this to event spec 46 | event := common_models.NewArtefactRequestEvent(artefact) 47 | 48 | err = h.messagingService.Publish(topic, event) 49 | if err != nil { 50 | log.Printf("Error publishing event: %v", err) 51 | return err 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (h *tapEventPublisher) HandleResponseHeaders(ctx context.Context, 58 | req *envoy_v3_ext_proc_pb.ProcessingRequest_ResponseHeaders) error { 59 | 60 | log.Printf("Publishing response headers event - NOP") 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /services/pkg/tap/tap_model.go: -------------------------------------------------------------------------------- 1 | package tap 2 | 3 | import ( 4 | "context" 5 | 6 | envoy_v3_ext_proc_pb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" 7 | ) 8 | 9 | type TapHandler interface { 10 | HandleRequestHeaders(context.Context, 11 | *envoy_v3_ext_proc_pb.ProcessingRequest_RequestHeaders) error 12 | HandleResponseHeaders(context.Context, 13 | *envoy_v3_ext_proc_pb.ProcessingRequest_ResponseHeaders) error 14 | } 15 | 16 | type TapHandlerRegistration struct { 17 | ContinueOnError bool 18 | Handler TapHandler 19 | } 20 | 21 | type TapHandlerChain struct { 22 | Handlers []TapHandlerRegistration 23 | } 24 | -------------------------------------------------------------------------------- /services/pkg/tap/tap_response.go: -------------------------------------------------------------------------------- 1 | package tap 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 7 | envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 8 | envoy_v3_ext_proc_pb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" 9 | ) 10 | 11 | const ( 12 | tapSignatureHeaderName = "x-gateway-tap" 13 | tapSignatureHeaderValue = "true" 14 | ) 15 | 16 | type tapResponse struct { 17 | pr *envoy_v3_ext_proc_pb.ProcessingResponse 18 | } 19 | 20 | func buildTapResponse() *tapResponse { 21 | return &tapResponse{ 22 | pr: &envoy_v3_ext_proc_pb.ProcessingResponse{}, 23 | } 24 | } 25 | 26 | func (r *tapResponse) Response() *envoy_v3_ext_proc_pb.ProcessingResponse { 27 | return r.pr 28 | } 29 | 30 | func (r *tapResponse) WithProcessingResponseRequestHeaders() *tapResponse { 31 | if r.pr.Response != nil { 32 | logger.Errorf("this tap response has a envoy processing response already set: %v", 33 | reflect.TypeOf(r.pr.Response)) 34 | return r 35 | } 36 | 37 | r.pr.Response = &envoy_v3_ext_proc_pb.ProcessingResponse_RequestHeaders{ 38 | RequestHeaders: &envoy_v3_ext_proc_pb.HeadersResponse{ 39 | Response: &envoy_v3_ext_proc_pb.CommonResponse{ 40 | Status: envoy_v3_ext_proc_pb.CommonResponse_CONTINUE, 41 | }, 42 | }, 43 | } 44 | 45 | return r 46 | } 47 | 48 | func (r *tapResponse) WithTapSignature() *tapResponse { 49 | if r.pr.Response == nil { 50 | logger.Errorf("this tap response is not initialized") 51 | return r 52 | } 53 | 54 | return r 55 | } 56 | 57 | func (r *tapResponse) SetResponseHeader(key, value string) *tapResponse { 58 | if res, ok := r.AsProcessingResponseResponseHeaders(); ok { 59 | res.ResponseHeaders.Response.HeaderMutation.SetHeaders = append(res.ResponseHeaders.Response.HeaderMutation.SetHeaders, 60 | &envoy_config_core_v3.HeaderValueOption{ 61 | Header: &envoy_config_core_v3.HeaderValue{ 62 | Key: tapSignatureHeaderName, 63 | Value: tapSignatureHeaderValue, 64 | }, 65 | }) 66 | } 67 | 68 | return r 69 | } 70 | 71 | func (r *tapResponse) AsProcessingResponseResponseHeaders() (*envoy_v3_ext_proc_pb.ProcessingResponse_ResponseHeaders, bool) { 72 | cr, ok := r.pr.Response.(*envoy_v3_ext_proc_pb.ProcessingResponse_ResponseHeaders) 73 | return cr, ok 74 | } 75 | 76 | func (r *tapResponse) AsProcessingResponseRequestHeaders() (*envoy_v3_ext_proc_pb.ProcessingResponse_RequestHeaders, bool) { 77 | cr, ok := r.pr.Response.(*envoy_v3_ext_proc_pb.ProcessingResponse_RequestHeaders) 78 | return cr, ok 79 | } 80 | 81 | func (r *tapResponse) WithProcessingResponseResponseHeaders() *tapResponse { 82 | if r.pr.Response != nil { 83 | logger.Errorf("this tap response has a envoy processing response already set: %v", 84 | reflect.TypeOf(r.pr.Response)) 85 | return r 86 | } 87 | 88 | r.pr.Response = &envoy_v3_ext_proc_pb.ProcessingResponse_ResponseHeaders{ 89 | ResponseHeaders: &envoy_v3_ext_proc_pb.HeadersResponse{ 90 | Response: &envoy_v3_ext_proc_pb.CommonResponse{ 91 | HeaderMutation: &envoy_v3_ext_proc_pb.HeaderMutation{ 92 | SetHeaders: []*envoy_config_core_v3.HeaderValueOption{}, 93 | }, 94 | }, 95 | }, 96 | } 97 | 98 | return r 99 | } 100 | -------------------------------------------------------------------------------- /services/pkg/tap/tap_service.go: -------------------------------------------------------------------------------- 1 | package tap 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/logger" 8 | "github.com/abhisek/supply-chain-gateway/services/pkg/common/messaging" 9 | envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 10 | envoy_v3_ext_proc_pb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | type tapService struct { 16 | handlerChain TapHandlerChain 17 | messagingService messaging.MessagingService 18 | } 19 | 20 | func NewTapService(msgService messaging.MessagingService, 21 | registrations []TapHandlerRegistration) (envoy_v3_ext_proc_pb.ExternalProcessorServer, error) { 22 | 23 | return &tapService{messagingService: msgService, 24 | handlerChain: TapHandlerChain{Handlers: registrations}}, nil 25 | } 26 | 27 | func (s *tapService) RegisterHandler(handler TapHandlerRegistration) { 28 | s.handlerChain.Handlers = append(s.handlerChain.Handlers, handler) 29 | } 30 | 31 | func (s *tapService) Process(srv envoy_v3_ext_proc_pb.ExternalProcessor_ProcessServer) error { 32 | log.Printf("Tap service: Handling stream") 33 | 34 | ctx := srv.Context() 35 | for { 36 | select { 37 | case <-ctx.Done(): 38 | logger.Errorf("Context is finished: %v", ctx.Err()) 39 | return ctx.Err() 40 | default: 41 | } 42 | 43 | req, err := srv.Recv() 44 | if err != nil { 45 | return status.Errorf(codes.Unknown, "Error receiving request: %v", err) 46 | } 47 | 48 | resp := &envoy_v3_ext_proc_pb.ProcessingResponse{} 49 | switch req.Request.(type) { 50 | case *envoy_v3_ext_proc_pb.ProcessingRequest_RequestHeaders: 51 | err = s.handleRequestHeaders(ctx, 52 | req.Request.(*envoy_v3_ext_proc_pb.ProcessingRequest_RequestHeaders)) 53 | 54 | resp.Response = &envoy_v3_ext_proc_pb.ProcessingResponse_RequestHeaders{ 55 | RequestHeaders: &envoy_v3_ext_proc_pb.HeadersResponse{ 56 | Response: &envoy_v3_ext_proc_pb.CommonResponse{ 57 | Status: envoy_v3_ext_proc_pb.CommonResponse_CONTINUE, 58 | }, 59 | }, 60 | } 61 | 62 | // TODO: Use handler chain for applying upstream auth 63 | err = s.applyUpstreamAuth(req.Request.(*envoy_v3_ext_proc_pb.ProcessingRequest_RequestHeaders), 64 | resp.Response.(*envoy_v3_ext_proc_pb.ProcessingResponse_RequestHeaders)) 65 | break 66 | case *envoy_v3_ext_proc_pb.ProcessingRequest_ResponseHeaders: 67 | err = s.handleResponseHeaders(ctx, 68 | req.Request.(*envoy_v3_ext_proc_pb.ProcessingRequest_ResponseHeaders)) 69 | s.addTapSignature(resp) 70 | break 71 | default: 72 | log.Printf("Unknown request type: %v", req.Request) 73 | } 74 | 75 | // TODO: How should we handle this behavior? 76 | if err != nil { 77 | log.Printf("Error in handling processing req: %v", err) 78 | } 79 | 80 | if err := srv.Send(resp); err != nil { 81 | log.Printf("Failed to send stream response: %v", err) 82 | } 83 | } 84 | } 85 | 86 | func (s *tapService) handleRequestHeaders(ctx context.Context, 87 | req *envoy_v3_ext_proc_pb.ProcessingRequest_RequestHeaders) error { 88 | for _, registration := range s.handlerChain.Handlers { 89 | err := registration.Handler.HandleRequestHeaders(ctx, req) 90 | if !registration.ContinueOnError && err != nil { 91 | log.Printf("Unable to continue on tap handler error: %v", err) 92 | return err 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (s *tapService) handleResponseHeaders(ctx context.Context, 100 | req *envoy_v3_ext_proc_pb.ProcessingRequest_ResponseHeaders) error { 101 | for _, registration := range s.handlerChain.Handlers { 102 | err := registration.Handler.HandleResponseHeaders(ctx, req) 103 | if !registration.ContinueOnError && err != nil { 104 | log.Printf("Unable to continue on tap handler error: %v", err) 105 | return err 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // Lets add a tap signature only if the response is not already used 113 | func (s *tapService) addTapSignature(resp *envoy_v3_ext_proc_pb.ProcessingResponse) { 114 | if resp.Response != nil { 115 | return 116 | } 117 | 118 | log.Printf("Adding tap signature to response headers") 119 | resp.Response = &envoy_v3_ext_proc_pb.ProcessingResponse_ResponseHeaders{ 120 | ResponseHeaders: &envoy_v3_ext_proc_pb.HeadersResponse{ 121 | Response: &envoy_v3_ext_proc_pb.CommonResponse{ 122 | HeaderMutation: &envoy_v3_ext_proc_pb.HeaderMutation{ 123 | SetHeaders: []*envoy_config_core_v3.HeaderValueOption{ 124 | { 125 | Header: &envoy_config_core_v3.HeaderValue{ 126 | Key: "x-gateway-tap", 127 | Value: "true", 128 | }, 129 | }, 130 | }, 131 | }, 132 | }, 133 | }, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /services/pkg/tap/tap_upstream_auth.go: -------------------------------------------------------------------------------- 1 | package tap 2 | 3 | import ( 4 | "log" 5 | 6 | common_models "github.com/abhisek/supply-chain-gateway/services/pkg/common/models" 7 | envoy_v3_ext_proc_pb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" 8 | ) 9 | 10 | func (s *tapService) applyUpstreamAuth(req *envoy_v3_ext_proc_pb.ProcessingRequest_RequestHeaders, 11 | resp *envoy_v3_ext_proc_pb.ProcessingResponse_RequestHeaders) error { 12 | 13 | host, path, err := findHostAndPath(req) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | upstream, err := common_models.GetUpstreamByHostAndPath(host, path) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | if !upstream.NeedUpstreamAuthentication() { 24 | log.Printf("Upstream %s do not need authentication", upstream.Name) 25 | return nil 26 | } 27 | 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /services/pkg/tap/tap_utils.go: -------------------------------------------------------------------------------- 1 | package tap 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | envoy_v3_ext_proc_pb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" 8 | ) 9 | 10 | // Header keys are stored as ":key" by envoy 11 | func findHeaderValue(req *envoy_v3_ext_proc_pb.ProcessingRequest_RequestHeaders, 12 | key string) (string, error) { 13 | 14 | for _, h := range req.RequestHeaders.Headers.Headers { 15 | if strings.EqualFold(":"+key, h.Key) { 16 | return h.Value, nil 17 | } 18 | } 19 | 20 | return "", fmt.Errorf("header with key: %s not found", key) 21 | } 22 | 23 | func findHostAndPath(req *envoy_v3_ext_proc_pb.ProcessingRequest_RequestHeaders) (string, string, error) { 24 | path, err := findHeaderValue(req, "path") 25 | if err != nil { 26 | return "", "", fmt.Errorf("failed to find path in req: %w", err) 27 | } 28 | 29 | // https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.3 30 | host, err := findHeaderValue(req, "authority") 31 | if err != nil { 32 | return "", "", fmt.Errorf("failed to find host in req: %w", err) 33 | } 34 | 35 | return host, path, nil 36 | } 37 | -------------------------------------------------------------------------------- /services/spec/proto/config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | option go_package = "github.com/abhisek/supply-chain-gateway/services/gen"; 5 | 6 | import "validate/validate.proto"; 7 | 8 | // Proto3 lacks string enums. 9 | // The upstream type is used by PDP to construct artifact struct 10 | // by parsing the path name as per ecosystem convention 11 | // Aligning the ecosystem with OSV schema 12 | // https://ossf.github.io/osv-schema/#affectedpackage-field 13 | enum GatewayUpstreamType { 14 | InvalidUpStreamType = 0; 15 | Maven = 100; 16 | Npm = 200; 17 | PyPI = 300; 18 | RubyGems = 400; 19 | Go = 500; 20 | } 21 | 22 | // To allow default upstreams to be attached 23 | enum GatewayUpstreamManagementType { 24 | InvalidManagementType = 0; 25 | EnvironmentAdmin = 100; 26 | GatewayAdmin = 200; 27 | } 28 | 29 | enum GatewayAuthenticationType { 30 | InvalidAuthType = 0; 31 | NoAuth = 100; 32 | Basic = 200; 33 | OIDC = 300; 34 | } 35 | 36 | enum GatewaySecretSource { 37 | InvalidSecretSource = 0; 38 | Environment = 100; 39 | } 40 | 41 | message GatewayAuthenticationProvider { 42 | GatewayAuthenticationType type = 1; 43 | string provider = 2; 44 | } 45 | 46 | // Route matching algorithm to decide the order of matching 47 | message GatewayUpstreamRoute { 48 | string host = 1; 49 | string path_prefix = 2; 50 | string path_pattern = 3; 51 | 52 | string host_rewrite_value = 4; 53 | string path_prefix_rewrite_value = 5; 54 | } 55 | 56 | message GatewayUpstreamRepository { 57 | string host = 1; 58 | string port = 2; 59 | bool tls = 3; 60 | string sni = 4; 61 | GatewayAuthenticationProvider authentication = 5; 62 | } 63 | 64 | message GatewayInfo { 65 | string id = 1; 66 | string name = 2; 67 | string domain = 3; 68 | } 69 | 70 | message GatewayUpstream { 71 | GatewayUpstreamType type = 1; 72 | GatewayUpstreamManagementType management_type = 2; 73 | string name = 3; 74 | GatewayAuthenticationProvider authentication = 4; 75 | GatewayUpstreamRoute route = 5; 76 | GatewayUpstreamRepository repository = 6; 77 | } 78 | 79 | // Credentials should NOT be plain text 80 | // but interpreted as htpasswd bcrypt encrypted 81 | // https://httpd.apache.org/docs/2.4/programs/htpasswd.html 82 | message GatewayAuthenticatorBasicAuth { 83 | map credentials = 1; 84 | 85 | // Load from file source 86 | // This can be a local file or an object storage path such as S3 87 | // depending on the capability of the repository loading 88 | // the configuration data 89 | string path = 2; 90 | } 91 | 92 | message GatewayAuthenticator { 93 | GatewayAuthenticationType type = 1; 94 | 95 | oneof config { 96 | GatewayAuthenticatorBasicAuth basic_auth = 2; 97 | } 98 | } 99 | 100 | message GatewaySecretSourceEnvironment { 101 | string key = 1; 102 | } 103 | 104 | message GatewaySecret { 105 | GatewaySecretSource source = 1; 106 | oneof value { 107 | GatewaySecretSourceEnvironment environment = 2; 108 | } 109 | } 110 | 111 | message MessagingAdapter { 112 | enum AdapterType { 113 | NATS = 0; 114 | KAFKA = 1; 115 | } 116 | 117 | message NatsAdapterConfig { 118 | string url = 1; 119 | } 120 | 121 | message KafkaAdapterConfig { 122 | repeated string bootstrap_servers = 1; 123 | string schema_registry_url = 2; 124 | } 125 | 126 | AdapterType type = 1; 127 | 128 | // https://developers.google.com/protocol-buffers/docs/proto#oneof 129 | oneof config { 130 | NatsAdapterConfig nats = 2; 131 | KafkaAdapterConfig kafka = 3; 132 | } 133 | } 134 | 135 | message TapServiceConfig { 136 | message PublisherConfig { 137 | message TopicNames { 138 | string upstream_request = 1; 139 | string upstream_response = 2; 140 | } 141 | 142 | string messaging_adapter_name = 1; 143 | TopicNames topic_names = 2; 144 | } 145 | 146 | PublisherConfig publisher_config = 1; 147 | } 148 | 149 | enum PdsClientType { 150 | LOCAL = 0; 151 | RAYA = 1; 152 | } 153 | 154 | message PdsClientCommonConfig { 155 | string host = 1; 156 | int32 port = 2; 157 | bool mtls = 3; 158 | } 159 | 160 | message PdsClientConfig { 161 | PdsClientType type = 1; 162 | oneof config { 163 | PdsClientCommonConfig common = 2; 164 | }; 165 | } 166 | 167 | message PdpServiceConfig { 168 | message PublisherConfig { 169 | message TopicNames { 170 | string policy_audit = 1 [(validate.rules).string = {min_len: 3, max_len: 255}]; 171 | } 172 | 173 | string messaging_adapter_name = 1 [(validate.rules).string = {min_len: 3, max_len: 255}]; 174 | TopicNames topic_names = 2 [(validate.rules).message.required = true]; 175 | } 176 | 177 | bool monitor_mode = 1; 178 | PdsClientConfig pds_client = 2; 179 | PublisherConfig publisher_config = 3; 180 | } 181 | 182 | message DcsServiceConfig { 183 | enum StorageAdapterType { 184 | MYSQL = 0; 185 | } 186 | 187 | bool active = 1; 188 | StorageAdapterType storage_type = 2; 189 | 190 | // TODO: Introduce storage configurations similar to messaging 191 | // Services can refer to the config by name 192 | string storage_adapter_config_name = 3; // [(validate.rules).string = {min_len: 3, max_len: 255}] 193 | 194 | string messaging_adapter_name = 4 [(validate.rules).string = {min_len: 3, max_len: 255}]; 195 | } 196 | 197 | message GatewayConfiguration { 198 | message Listener { 199 | string host = 1 [(validate.rules).string = {min_len: 7, max_len: 255}]; 200 | uint32 port = 2 [(validate.rules).uint32.lt = 65535]; 201 | } 202 | 203 | GatewayInfo info = 1 [(validate.rules).message.required = true]; 204 | Listener listener = 2 [(validate.rules).message.required = true]; 205 | repeated GatewayUpstream upstreams = 3; 206 | map authenticators = 4; 207 | map secrets = 5; 208 | map messaging = 6; 209 | 210 | message ServiceConfig { 211 | PdpServiceConfig pdp = 1; 212 | TapServiceConfig tap = 2; 213 | DcsServiceConfig dcs = 3; 214 | } 215 | 216 | ServiceConfig services = 7 [(validate.rules).message.required = true]; 217 | } 218 | -------------------------------------------------------------------------------- /services/spec/proto/events.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | 5 | option go_package = "github.com/abhisek/supply-chain-gateway/services/gen"; 6 | 7 | import "models.proto"; 8 | 9 | // Align with 10 | // https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/timestamp.proto 11 | message EventTimestamp { 12 | int64 seconds = 1; 13 | int32 nanos = 2; 14 | } 15 | 16 | // Use this for all contextual metadata 17 | message EventContext { 18 | string org_id = 1; // 0 indicate unsegmented 19 | string project_id = 2; // 0 indicate unsegmented 20 | string gateway_domain = 3; 21 | string env_domain = 4; 22 | } 23 | 24 | // Enumeration of all Event Types 25 | enum EventType { 26 | ErrorNoSuchEvent = 0; 27 | PolicyEvaluationAuditEvent = 1; 28 | } 29 | 30 | // Event header and metadata 31 | message EventHeader { 32 | EventType type = 1; // Type of the event 33 | string id = 2; // Unique event ID 34 | string source = 3; // Which service generated this event 35 | 36 | EventContext context = 4; 37 | 38 | // Deprecated 39 | EventTimestamp created_at = 5; // The timestamp of when this event was created 40 | } 41 | 42 | // Represents an event when PDP completes a Policy Evaluation 43 | message PolicyEvaluationEvent { 44 | EventHeader header = 1; 45 | 46 | message Data { 47 | Artefact artefact = 1; // Included from models.proto 48 | ArtefactUpstream upstream = 2; // Included from models.proto 49 | 50 | message ArtefactEnrichments { 51 | message ArtefactAdvisory { 52 | string source = 1; 53 | string source_id = 2; 54 | string severity = 3; 55 | string title = 4; 56 | } 57 | 58 | message ArtefactProjectScorecard { 59 | message Repo { 60 | string name = 1; 61 | string commit = 2; 62 | } 63 | 64 | message Check { 65 | string reason = 1; 66 | float score = 2; 67 | } 68 | 69 | int64 timestamp = 1; 70 | float score = 2; 71 | Repo repo = 3; 72 | map checks = 4; 73 | string version = 5; 74 | } 75 | 76 | repeated string licenses = 1; 77 | repeated ArtefactAdvisory advisories = 2; 78 | ArtefactProjectScorecard scorecard = 3; 79 | } 80 | 81 | message Result { 82 | message Violation { 83 | int32 code = 1; 84 | string message = 2; 85 | } 86 | 87 | message PackageMetaQueryStatus { 88 | string code = 1; 89 | string message = 2; 90 | } 91 | 92 | bool policy_allowed = 1; // Did the policy allow this artifact? 93 | bool effective_allowed = 2; // Did the gateway allow this artifact? 94 | bool monitor_mode = 3; // The boolean flag for monitor mode config 95 | 96 | repeated Violation violations = 4; // Array of violation 97 | 98 | PackageMetaQueryStatus package_query_status = 5; // Status of package meta query 99 | } 100 | 101 | Result result = 3; 102 | 103 | string username = 4; 104 | 105 | // Include other enriched metadata 106 | // This should be ideally moved to data layer 107 | ArtefactEnrichments enrichments = 5; 108 | } 109 | 110 | Data data = 2; 111 | int64 timestamp = 3; // The timestamp in unix millis format 112 | } 113 | -------------------------------------------------------------------------------- /services/spec/proto/lib/google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/api/http.proto"; 20 | import "google/protobuf/descriptor.proto"; 21 | 22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "AnnotationsProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | extend google.protobuf.MethodOptions { 29 | // See `HttpRule`. 30 | HttpRule http = 72295728; 31 | } 32 | -------------------------------------------------------------------------------- /services/spec/proto/models.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | 5 | option go_package = "github.com/abhisek/supply-chain-gateway/services/gen"; 6 | 7 | import "validate/validate.proto"; 8 | 9 | message Artefact { 10 | // https://ossf.github.io/osv-schema/#affectedpackage-field 11 | string ecosystem = 1; // Ecosystem name aligned with OpenSSF schema 12 | string group = 2; 13 | string name = 3; 14 | string version = 4; 15 | } 16 | 17 | message ArtefactUpstream { 18 | string id = 1; // Unique ID 19 | string type = 2; // The type of the upstream 20 | string name = 3; // User defined name for the upstream 21 | } 22 | 23 | enum VulnerabilitySeverity { 24 | UNKNOWN_SEVERITY = 0; 25 | CRITICAL = 10; 26 | HIGH = 20; 27 | MEDIUM = 30; 28 | LOW = 40; 29 | INFO = 50; 30 | } 31 | 32 | message VulnerabilityScore { 33 | string type = 1; 34 | string value = 2; 35 | } 36 | 37 | message VulnerabilityReference { 38 | string type = 1; 39 | string url = 2; 40 | } 41 | 42 | message VulnerabilityMeta { 43 | string id = 1; 44 | string source = 2; 45 | string title = 3; 46 | VulnerabilitySeverity severity = 4; 47 | repeated VulnerabilityScore scores = 5; 48 | } 49 | 50 | message VulnerabilityDetail { 51 | string id = 1; 52 | VulnerabilityMeta meta = 2; 53 | } 54 | -------------------------------------------------------------------------------- /services/spec/proto/pds.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | 5 | option go_package = "github.com/abhisek/supply-chain-gateway/services/gen"; 6 | 7 | import "validate/validate.proto"; 8 | import "models.proto"; 9 | 10 | message FindVulnerabilityByArtefactRequest { 11 | Artefact artefact = 1 [(validate.rules).message.required = true]; 12 | } 13 | 14 | message VulnerabilityList { 15 | repeated VulnerabilityMeta vulnerabilities = 1 [(validate.rules).repeated = { ignore_empty: true }]; 16 | } 17 | 18 | message EnrichedArtefact { 19 | 20 | } 21 | 22 | message GetVulnerabilityByIdRequest { 23 | string id = 1; 24 | } 25 | 26 | service PolicyDataService { 27 | /* 28 | Find applicable vulerabilties by an Artefact meta information. Ecosystem, Group, Name is used 29 | as composite lookup key for retrieving all vulnerabilities for a given artefact from the DB. 30 | Subsequently perform an in-memory fuzzy match to select all the vulnerabilities matching the 31 | requested artefact version. 32 | */ 33 | rpc FindVulnerabilitiesByArtefact(FindVulnerabilityByArtefactRequest) returns(VulnerabilityList) {} 34 | 35 | /* 36 | Get all details of the vulnerabilty available in the data source. Primarily meant for frontend 37 | requirement or for reporting purpose. 38 | */ 39 | rpc GetVulnerabilityDetails(GetVulnerabilityByIdRequest) returns (VulnerabilityDetail) {} 40 | } 41 | -------------------------------------------------------------------------------- /services/spec/proto/raya.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package raya; 4 | 5 | option go_package = "github.com/abhisek/supply-chain-gateway/services/gen"; 6 | 7 | import "validate/validate.proto"; 8 | 9 | message KV { 10 | string key = 1; 11 | string value = 2; 12 | } 13 | 14 | message Package { 15 | string ecosystem = 1; // Package ecosystem 16 | string name = 2; // nokogiri, requests, com.google:gson 17 | } 18 | 19 | message PackageVersion { 20 | Package package = 1; 21 | string version = 2; 22 | } 23 | 24 | message PackageAdvisoryIdentifier { 25 | string type = 1; 26 | string id = 2; 27 | } 28 | 29 | enum Severity { 30 | UNKNOWN = 0; 31 | INFO = 1; 32 | LOW = 2; 33 | MEDIUM = 3; 34 | HIGH = 4; 35 | CRITICAL = 5; 36 | } 37 | 38 | message PackageAdvisorySeverity { 39 | Severity severity = 1; // Computed severity to be documented 40 | 41 | string github_severity = 2; 42 | float cvssv3_score = 3; 43 | } 44 | 45 | message PackageAdvisory { 46 | string source = 1; 47 | string source_id = 2; 48 | string title = 3; 49 | 50 | repeated PackageAdvisoryIdentifier identifiers = 7; 51 | PackageAdvisorySeverity advisory_severity = 8; 52 | } 53 | 54 | message PackageVersionMetaQueryRequest { 55 | PackageVersion package_version = 1; 56 | } 57 | 58 | /** 59 | Scorecard Specification for a project if exists 60 | **/ 61 | 62 | message ProjectScorecardCheck { 63 | string reason = 1; 64 | float score = 2; 65 | } 66 | 67 | message ProjectScorecardChecks { 68 | ProjectScorecardCheck binary_artifacts = 1; 69 | ProjectScorecardCheck branch_protection = 2; 70 | ProjectScorecardCheck cii_best_practices = 3; 71 | ProjectScorecardCheck code_review = 4; 72 | ProjectScorecardCheck dangerous_workflow = 5; 73 | ProjectScorecardCheck dependency_update_tool = 6; 74 | ProjectScorecardCheck fuzzing = 7; 75 | ProjectScorecardCheck license = 8; 76 | ProjectScorecardCheck maintained = 9; 77 | ProjectScorecardCheck packaging = 10; 78 | ProjectScorecardCheck pinned_dependencies = 11; 79 | ProjectScorecardCheck sast = 12; 80 | ProjectScorecardCheck security_policy = 13; 81 | ProjectScorecardCheck signed_releases = 14; 82 | ProjectScorecardCheck vulnerabilities = 15; 83 | ProjectScorecardCheck token_permissions = 16; 84 | } 85 | 86 | message ProjectScorecardRepo { 87 | string name = 1; 88 | string commit = 2; 89 | } 90 | 91 | message ProjectScorecard { 92 | uint64 timestamp = 1; 93 | float score = 2; 94 | ProjectScorecardRepo repo = 3; 95 | ProjectScorecardChecks checks = 4; 96 | string version = 5; 97 | } 98 | 99 | message PackageVersionMetaQueryResponse { 100 | PackageVersion package_version = 1; 101 | 102 | repeated string licenses = 2; // SPDX license names 103 | repeated PackageAdvisory advisories = 3; // Vulnerabilities 104 | ProjectScorecard project_scorecard = 4; // Project scorecard 105 | } 106 | 107 | service Raya { 108 | rpc GetPackageMetaByVersion(PackageVersionMetaQueryRequest) returns(PackageVersionMetaQueryResponse) {} 109 | } 110 | 111 | /** 112 | We need 2 types of APIs 113 | 1. Meta query which returns a single result ideally 114 | 2. List or retrieve operation that returns multiple data 115 | */ 116 | --------------------------------------------------------------------------------