├── .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 | 
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 |
--------------------------------------------------------------------------------