Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6acf08aec0 | ||
|
|
33c4ca5059 | ||
|
|
a430641960 | ||
|
|
76788aa1cb | ||
|
|
dbc2eadac3 | ||
|
|
ecf8c93f3f | ||
|
|
070c5f5a24 | ||
|
|
4e39cebe61 | ||
|
|
767e7cb16e | ||
|
|
4da939276e | ||
|
|
dc5846fb93 | ||
|
|
18a996dd53 | ||
|
|
333729a032 | ||
|
|
0a67953b3f | ||
|
|
3316c81f35 | ||
|
|
51e185a09f | ||
|
|
4599bacf17 | ||
|
|
e971466097 | ||
|
|
65c44c97c1 | ||
|
|
1c824c5515 | ||
|
|
e1908e783b | ||
|
|
6f5d0cea33 | ||
|
|
dc3bc11a51 | ||
|
|
9aebb9d68e | ||
|
|
801e81ac0d | ||
|
|
aa06dc6d93 | ||
|
|
8e02db1d09 | ||
|
|
6ad8539938 | ||
|
|
6ade12a5f6 | ||
|
|
521a5a5756 | ||
|
|
54bade56e9 | ||
|
|
1aecf640aa | ||
|
|
b1738ac35a | ||
|
|
5663b59636 | ||
|
|
dc14669605 | ||
|
|
a8cd3a5b08 | ||
|
|
2d74224fd3 | ||
|
|
b4aaf026c9 | ||
|
|
e48452dd05 | ||
|
|
9f55120d2d | ||
|
|
f88f76a1c5 | ||
|
|
08e8915242 | ||
|
|
d5a64a4f61 | ||
|
|
8671c25fb5 | ||
|
|
3eb501c72b | ||
|
|
498f809e94 | ||
|
|
c2cb4375f8 | ||
|
|
7f8b335393 | ||
|
|
969404956c | ||
|
|
e9d3da03db | ||
|
|
ec792952f8 | ||
|
|
d94a05844e | ||
|
|
c449d94dad | ||
|
|
c0144e60e3 | ||
|
|
d3c87b6e06 | ||
|
|
0ae3560ba1 | ||
|
|
e4e9dc2590 | ||
|
|
f2a4722d04 | ||
|
|
850f2c1dc9 | ||
|
|
6dcbd15923 | ||
|
|
82505f2460 | ||
|
|
1afeb397f7 | ||
|
|
5844d4ffd1 | ||
|
|
651b09b085 | ||
|
|
118210c7c7 | ||
|
|
269b843bb7 | ||
|
|
68609f7397 | ||
|
|
d0d21e6710 | ||
|
|
319cfdac5e | ||
|
|
458391e6f0 | ||
|
|
c8ad28bf1a | ||
|
|
3791b9555c | ||
|
|
30a7b484d1 | ||
|
|
41684afa80 | ||
|
|
2d06114eb7 | ||
|
|
52aa033fe4 | ||
|
|
d8b83e2bc2 | ||
|
|
b9ebcc71da | ||
|
|
0f4c01b83a | ||
|
|
7cb923b2fd | ||
|
|
e32784a09e | ||
|
|
db6832e782 | ||
|
|
4b80c4a624 | ||
|
|
12785f9c05 | ||
|
|
7d4569fb3d | ||
|
|
47939fafaf | ||
|
|
e68ead129b | ||
|
|
742365bc18 | ||
|
|
dee54e5184 | ||
|
|
1e84769891 | ||
|
|
16a67216b4 | ||
|
|
0b4e76d858 | ||
|
|
ccd2c48e0c | ||
|
|
150cad94a6 | ||
|
|
c488d980cc | ||
|
|
3d8b7c5b63 | ||
|
|
b4976630da | ||
|
|
3d6d8ed036 | ||
|
|
075627f53f | ||
|
|
1e2b75fa9c | ||
|
|
06988f9b33 | ||
|
|
c3d96dbef5 | ||
|
|
5f4cccbc85 | ||
|
|
cd7caffb4a | ||
|
|
548ede77b8 | ||
|
|
3fa7307d93 | ||
|
|
5cc80f9f88 | ||
|
|
387f6aeb72 | ||
|
|
200257bab3 | ||
|
|
a87e6ab466 | ||
|
|
a0f6467e85 | ||
|
|
78e2bad49b | ||
|
|
4f989c59f6 | ||
|
|
932269d6bb | ||
|
|
7a2a365136 | ||
|
|
459a246488 | ||
|
|
966971b941 | ||
|
|
f9d17487ad | ||
|
|
8b71c540e6 | ||
|
|
cccf646856 | ||
|
|
124199b331 | ||
|
|
5a5a9518c6 | ||
|
|
3d94f0f710 | ||
|
|
4f43177976 | ||
|
|
0a2d642ea8 | ||
|
|
49cc6ca395 | ||
|
|
a2c003f634 | ||
|
|
72e08bdd8a | ||
|
|
1f8a5750d2 | ||
|
|
826b8eafdf | ||
|
|
5c7ab43567 | ||
|
|
a04cf78657 | ||
|
|
9e9abdd162 | ||
|
|
5840ebd227 | ||
|
|
f2a2eb0548 | ||
|
|
ac1c83393f | ||
|
|
a77be8f4c4 | ||
|
|
eebae09e82 | ||
|
|
e90756d5a3 | ||
|
|
c795255e35 | ||
|
|
36d5c021f3 | ||
|
|
3490812ed1 | ||
|
|
5b766bb27b | ||
|
|
0bdbf2da8a | ||
|
|
00eb5c8ec7 | ||
|
|
0a3fc8d428 | ||
|
|
3d7ea62f61 | ||
|
|
88f5178a05 | ||
|
|
3d9639d0ef | ||
|
|
03e1a10c1d | ||
|
|
7f88f83a8c | ||
|
|
0d1ba75d4c | ||
|
|
2e0c4a5d3d | ||
|
|
923fc4744c | ||
|
|
859ce29ab0 | ||
|
|
0ad855f2a8 | ||
|
|
f6797d0d6f | ||
|
|
6cf88c2e86 | ||
|
|
aaa1794e89 | ||
|
|
0bfd37027f | ||
|
|
d7f7a6c72e | ||
|
|
976c5317ac | ||
|
|
86382bbc70 | ||
|
|
d0111cec20 | ||
|
|
95c2970e6d | ||
|
|
bf2316843b | ||
|
|
02fa8de1b4 | ||
|
|
828467309c | ||
|
|
c156f54274 | ||
|
|
f6258fdc86 | ||
|
|
635baeb86e | ||
|
|
6eec7e1740 | ||
|
|
18b9ffee84 | ||
|
|
ed80658f46 | ||
|
|
21a41aa3e8 | ||
|
|
05e6fbebfd | ||
|
|
c8d4cd38c9 | ||
|
|
35b0021569 | ||
|
|
4be215d914 | ||
|
|
c1d9050d26 | ||
|
|
14ca44453f | ||
|
|
2c682bdf66 | ||
|
|
855f0afb21 | ||
|
|
49b2e0f3df | ||
|
|
5387a395a3 | ||
|
|
ec141c3693 | ||
|
|
e0bb4a7836 | ||
|
|
27548de0c8 | ||
|
|
666011a975 | ||
|
|
5b4ae39c6a | ||
|
|
9bd1fb019b | ||
|
|
1e631efb06 | ||
|
|
5f960dbf6c | ||
|
|
c9bb677999 | ||
|
|
fdb3bdd0f2 | ||
|
|
ec02875f75 | ||
|
|
2f4e02e883 | ||
|
|
71fce1b1b2 | ||
|
|
1864e687f8 | ||
|
|
d599ac8407 | ||
|
|
1db3d0d2cf | ||
|
|
359712bee0 | ||
|
|
c313485e2f | ||
|
|
99612906d8 |
24
.cursor/mcp.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"pinner-mcp-stdio-server": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"ghcr.io/safedep/pinner-mcp:latest"
|
||||
]
|
||||
},
|
||||
"vet-mcp": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"ghcr.io/safedep/vet:latest",
|
||||
"server",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
10
.cursor/rules/vet-packages.mdc
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
Always use vet-mcp to vet any open source library package suggested by AI generated code before installation.
|
||||
Follow the rules below to use vet-mcp:
|
||||
|
||||
- Try to find the latest version. If not found, it is likely a slopsquatting vulnerability
|
||||
- Always check for critical vulnerabilities and malware
|
||||
80
.github/workflows/ci.yml
vendored
@ -3,29 +3,60 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-generated-code:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Run code generation
|
||||
run: make generate
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
run: |
|
||||
if [[ -n $(git status --porcelain) ]]; then
|
||||
echo "ERROR: Generated code is out of sync!"
|
||||
echo "Please run 'make generate' and commit the changes."
|
||||
echo ""
|
||||
echo "Files with changes:"
|
||||
git status --porcelain
|
||||
echo ""
|
||||
echo "Diff:"
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run-test:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568
|
||||
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
|
||||
with:
|
||||
go-version: 1.23
|
||||
check-latest: true
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Build and Test
|
||||
run: |
|
||||
go mod tidy
|
||||
go build
|
||||
go test -v ./...
|
||||
go test -coverprofile=coverage.txt -v ./...
|
||||
env:
|
||||
VET_E2E: true
|
||||
|
||||
@ -33,17 +64,23 @@ jobs:
|
||||
# test suites that use GitHub API
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Coverage
|
||||
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push'
|
||||
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
run-e2e:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568
|
||||
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
|
||||
with:
|
||||
go-version: 1.23
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Build vet
|
||||
@ -68,21 +105,34 @@ jobs:
|
||||
VET_API_KEY: ${{ secrets.SAFEDEP_CLOUD_API_KEY }}
|
||||
VET_CONTROL_TOWER_TENANT_ID: ${{ secrets.SAFEDEP_CLOUD_TENANT_DOMAIN }}
|
||||
|
||||
build-container:
|
||||
build-container-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
|
||||
- name: Build Container Image
|
||||
- name: Build Multi-Platform Container Image (verification only)
|
||||
run: |
|
||||
docker buildx build --platform linux/amd64 --platform linux/arm64 \
|
||||
-t build-container-test .
|
||||
docker buildx build --platform linux/amd64,linux/arm64 \
|
||||
-t build-container-test:latest .
|
||||
|
||||
- name: Build and Load Native Platform Image for Testing
|
||||
run: |
|
||||
docker buildx build --platform linux/amd64 --load \
|
||||
-t build-container-test:latest .
|
||||
|
||||
- name: Test Container Image
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e VET_API_KEY=${{ secrets.SAFEDEP_CLOUD_API_KEY }} \
|
||||
-e VET_CONTROL_TOWER_TENANT_ID=${{ secrets.SAFEDEP_CLOUD_TENANT_DOMAIN }} \
|
||||
build-container-test:latest \
|
||||
auth verify
|
||||
|
||||
16
.github/workflows/codeql.yml
vendored
@ -13,10 +13,10 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -35,20 +35,20 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
language: ["go"]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568
|
||||
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34
|
||||
with:
|
||||
go-version: 1.23
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@ -63,6 +63,6 @@ jobs:
|
||||
go build
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
112
.github/workflows/container.yml
vendored
@ -27,28 +27,130 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Registry Login
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55
|
||||
|
||||
- name: Build and Push Container Image
|
||||
run: |
|
||||
docker buildx build --push --platform linux/amd64 --platform linux/arm64 \
|
||||
# Get the tag if this was a tag push event
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
TAG=${{ github.ref_name }}
|
||||
# Validate tag format (must be vX.Y.Z)
|
||||
if [[ $TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
# Build and push with both version tag and latest
|
||||
docker buildx build --push --platform linux/amd64,linux/arm64 \
|
||||
-t $REGISTRY/$IMAGE_NAME:$TAG \
|
||||
-t $REGISTRY/$IMAGE_NAME:latest \
|
||||
.
|
||||
else
|
||||
echo "Invalid tag format. Must be in format vX.Y.Z (e.g. v1.2.3)"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# For non-tag pushes, just use latest tag
|
||||
docker buildx build --push --platform linux/amd64,linux/arm64 \
|
||||
-t $REGISTRY/$IMAGE_NAME:latest \
|
||||
.
|
||||
fi
|
||||
|
||||
publish-mcp-registry:
|
||||
if: startsWith(github.ref, 'refs/tags/') # only run this when new tag is publish
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # Required for OIDC authentication
|
||||
contents: read
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./.mcp-publisher
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Ensure jq is installed
|
||||
run: sudo apt-get update && sudo apt-get install -y jq
|
||||
|
||||
- name: Get version from tag
|
||||
# Strip 'v' prefix from tag (e.g., v1.0.0 -> 1.0.0) as
|
||||
# - we want clean version (x.y.z) without v prefix, since its already added by registry UI
|
||||
# - in case of docker image, we hardcode in server.json docker image identifier
|
||||
run: echo "VET_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: fill version in server.json
|
||||
run: sed -i "s/VERSION_FROM_ENV/$VET_VERSION/g" server.json
|
||||
|
||||
# publish mcp server
|
||||
- name: Install mcp-publisher
|
||||
run: |
|
||||
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
||||
|
||||
- name: Authenticate to MCP Registry
|
||||
run: ./mcp-publisher login github-oidc
|
||||
|
||||
- name: Publish server to MCP Registry
|
||||
run: ./mcp-publisher publish
|
||||
|
||||
verify-publish-mcp-registry:
|
||||
needs: publish-mcp-registry
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Ensure jq is installed
|
||||
run: sudo apt-get update && sudo apt-get install -y jq
|
||||
|
||||
- name: Get version from tag
|
||||
# Strip 'v' prefix from tag (e.g., v1.0.0 -> 1.0.0) as
|
||||
# - we want clean version (x.y.z) without v prefix, since its already added by registry UI
|
||||
# - in case of docker image, we hardcode in server.json docker image identifier
|
||||
run: echo "VET_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Query MCP Registry and verify server is published
|
||||
env:
|
||||
SERVER_NAME: "io.github.safedep/vet-mcp"
|
||||
REGISTRY_URL: "https://registry.modelcontextprotocol.io/v0.1/servers"
|
||||
run: |
|
||||
export EXPECTED_VERSION=$VET_VERSION
|
||||
|
||||
echo "Checking MCP Registry for $SERVER_NAME"
|
||||
|
||||
# Query registry
|
||||
url="${REGISTRY_URL}?search=${SERVER_NAME}"
|
||||
echo "Requesting: $url"
|
||||
http_status=$(curl -s -o response.json -w "%{http_code}" "$url")
|
||||
if [ "$http_status" -ne 200 ]; then
|
||||
echo "Registry query failed with HTTP status $http_status"
|
||||
cat response.json || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pretty print the response for debugging
|
||||
echo "Registry response (truncated):"
|
||||
jq 'if .servers then {servers: (.servers | length)} else . end' response.json
|
||||
|
||||
# Check for name and version match
|
||||
jq -e --arg name "$SERVER_NAME" --arg ver "$EXPECTED_VERSION" 'any(.servers[]; .server.name == $name and .server.version == $ver)' response.json >/dev/null || {
|
||||
echo "ERROR: Server $SERVER_NAME with version $EXPECTED_VERSION not found"
|
||||
echo "Full response:"
|
||||
cat response.json
|
||||
exit 1
|
||||
}
|
||||
echo "Found server $SERVER_NAME with version $EXPECTED_VERSION"
|
||||
|
||||
10
.github/workflows/dependency-review.yml
vendored
@ -4,7 +4,7 @@
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
|
||||
name: 'Dependency Review'
|
||||
name: "Dependency Review"
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@cc4f6536e38d1126c5e3b0683d469a14f23bfea4 # v3
|
||||
|
||||
65
.github/workflows/gh-pages-deploy.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
name: CLI Reference Manual GitHub Pages Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
SOURCE_GEN_DIR: ./docs/manual
|
||||
|
||||
jobs:
|
||||
# Build Jekkll (md -> html)
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Build vet
|
||||
run: go build
|
||||
|
||||
- name: Generate MD Docs in ${{ env.SOURCE_GEN_DIR }}
|
||||
run:
|
||||
./vet doc generate --markdown ${{ env.SOURCE_GEN_DIR }}
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Build with Jekyll
|
||||
uses: actions/jekyll-build-pages@v1
|
||||
with:
|
||||
source: ${{ env.SOURCE_GEN_DIR }}
|
||||
destination: ./_site
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
needs: build
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
15
.github/workflows/golangci-lint.yml
vendored
@ -6,23 +6,22 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
|
||||
with:
|
||||
go-version: 1.23
|
||||
cache: false
|
||||
- uses: actions/checkout@v3
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
|
||||
with:
|
||||
version: latest
|
||||
args: --issues-exit-code=1 --timeout=10m
|
||||
only-new-issues: true
|
||||
skip-pkg-cache: true
|
||||
skip-build-cache: true
|
||||
|
||||
|
||||
40
.github/workflows/goreleaser.yml
vendored
@ -3,7 +3,7 @@ name: Release Automation
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
concurrency: ci-release-automation
|
||||
|
||||
@ -34,9 +34,9 @@ jobs:
|
||||
- uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2
|
||||
- uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 # v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
|
||||
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34
|
||||
with:
|
||||
go-version: 1.23
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- name: ghcr-login
|
||||
uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7 # v1
|
||||
@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
- name: Setup Cache for OSX Cross Compiler Tool Chain
|
||||
id: osxcross-cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f # v3
|
||||
with:
|
||||
key: ${{ runner.os }}-osxcross-${{ env.OSX_CROSS_MACOS_SDK_VERSION }}
|
||||
path: |
|
||||
@ -75,27 +75,31 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: '~> v2'
|
||||
version: "~> v2"
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
|
||||
- name: Upload dist folder
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: dist-artifacts
|
||||
path: dist/
|
||||
|
||||
- name: Generate subject
|
||||
id: hash
|
||||
env:
|
||||
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
|
||||
echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT"
|
||||
provenance:
|
||||
needs: [goreleaser]
|
||||
permissions:
|
||||
actions: read # To read the workflow path.
|
||||
id-token: write # To sign the provenance.
|
||||
contents: write # To add assets to a release.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
|
||||
attestations: write # To write attestations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download dist folder
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
|
||||
upload-assets: true
|
||||
private-repository: false
|
||||
name: dist-artifacts
|
||||
path: dist/
|
||||
|
||||
- name: Attest build provenance (checksums)
|
||||
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||
with:
|
||||
subject-checksums: dist/checksums.txt
|
||||
|
||||
63
.github/workflows/publish-npm.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
name: Publish NPM Package
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Release Automation"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
publish-npm:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
outputs:
|
||||
package_version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Extract Tag Version
|
||||
id: version
|
||||
run: |
|
||||
TAG_VERSION="${{ github.event.workflow_run.head_branch }}"
|
||||
if [[ "$TAG_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
VERSION="${TAG_VERSION#v}" # Remove leading 'v'
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No valid tag found in head_branch: $TAG_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
- name: Prepare package
|
||||
run: |
|
||||
cd publish/npm
|
||||
npm version ${{ steps.version.outputs.version }} --no-git-tag-version
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
cd publish/npm
|
||||
npm publish --provenance
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
test-installation:
|
||||
needs: publish-npm
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node-version: ["14", "18", "20"]
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Test installation
|
||||
run: |
|
||||
npm install -g @safedep/vet@${{ needs.publish-npm.outputs.package_version }}
|
||||
vet version
|
||||
vet --help || true
|
||||
4
.github/workflows/scorecard.yml
vendored
@ -10,9 +10,9 @@ on:
|
||||
# To guarantee Maintained check is occasionally updated. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
||||
schedule:
|
||||
- cron: '35 22 * * 0'
|
||||
- cron: "35 22 * * 0"
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: ["main"]
|
||||
|
||||
# Declare default permissions as read only.
|
||||
permissions: read-all
|
||||
|
||||
8
.github/workflows/secret_scan.yml
vendored
@ -13,13 +13,13 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5
|
||||
with:
|
||||
fetch-depth: '0'
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
uses: trufflesecurity/trufflehog@8b6f55b592e46ac44a42dc3e3dee0ebcc0f56df5
|
||||
with:
|
||||
path: ./
|
||||
base: main
|
||||
head: HEAD
|
||||
base: ${{ github.event.pull_request.base.sha }}
|
||||
head: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
|
||||
7
.github/workflows/vet-ci.yml
vendored
@ -20,9 +20,9 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
- name: Enable Cloud Cloud
|
||||
- name: Enable Cloud Mode
|
||||
run: echo "SAFEDEP_CLOUD_MODE=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Override Cloud Mode if Actor is Dependabot
|
||||
@ -34,12 +34,13 @@ jobs:
|
||||
run: echo "SAFEDEP_CLOUD_MODE=false" >> $GITHUB_ENV
|
||||
|
||||
- name: Run vet
|
||||
uses: safedep/vet-action@v1
|
||||
uses: safedep/vet-action@01f547ee95dfd4f8f11fa64b399e5e00f22b0801
|
||||
with:
|
||||
policy: .github/vet/policy.yml
|
||||
cloud: ${{ env.SAFEDEP_CLOUD_MODE }}
|
||||
cloud-key: ${{ secrets.SAFEDEP_CLOUD_API_KEY }}
|
||||
cloud-tenant: ${{ secrets.SAFEDEP_CLOUD_TENANT_DOMAIN }}
|
||||
enable-comments-proxy: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SAFEDEP_CLOUD_MODE: ${{ env.SAFEDEP_CLOUD_MODE }}
|
||||
|
||||
36
.github/workflows/vet-container-scanning-e2e.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: Container Scan E2E
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
e2e-scan:
|
||||
name: E2E Scan on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Build and Test
|
||||
run: |
|
||||
make
|
||||
|
||||
- name: Run container scan tests
|
||||
shell: bash
|
||||
run: |
|
||||
./vet scan --image alpine:latest
|
||||
./vet scan --image ghcr.io/safedep/vet:latest
|
||||
./vet scan --image node:20
|
||||
7
.gitignore
vendored
@ -22,3 +22,10 @@
|
||||
/vet
|
||||
dist/
|
||||
/.env.dev
|
||||
.vscode/
|
||||
|
||||
# MacOS specific files
|
||||
**/.DS_Store
|
||||
|
||||
# Auto-generated context files
|
||||
CLAUDE.md
|
||||
|
||||
18
.golangci.yml
Normal file
@ -0,0 +1,18 @@
|
||||
# golangci-lint configuration file
|
||||
# See https://golangci-lint.run/usage/configuration/
|
||||
|
||||
version: "2"
|
||||
linters:
|
||||
exclusions:
|
||||
paths: []
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gci
|
||||
- gofumpt
|
||||
settings:
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- localmodule
|
||||
74
.mcp-publisher/server.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
|
||||
"name": "io.github.safedep/vet-mcp",
|
||||
"title": "SafeDep Vet MCP",
|
||||
"description": "Protect your AI agents and IDEs from malicious open-source packages.",
|
||||
"version": "VERSION_FROM_ENV",
|
||||
"websiteUrl": "https://safedep.io",
|
||||
"repository": {
|
||||
"url": "https://github.com/safedep/vet",
|
||||
"source": "github"
|
||||
},
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://raw.githubusercontent.com/safedep/.github/9275c7d1b59f718d73e47cecd93df92e7bfbea25/assets/logo/safedep-logo-darkshade.svg",
|
||||
"mimeType": "image/svg+xml",
|
||||
"sizes": [
|
||||
"48x48",
|
||||
"96x96"
|
||||
],
|
||||
"theme": "light"
|
||||
},
|
||||
{
|
||||
"src": "https://raw.githubusercontent.com/safedep/.github/9275c7d1b59f718d73e47cecd93df92e7bfbea25/assets/logo/safedep-logo.svg",
|
||||
"mimeType": "image/svg+xml",
|
||||
"sizes": [
|
||||
"48x48",
|
||||
"96x96"
|
||||
],
|
||||
"theme": "dark"
|
||||
}
|
||||
],
|
||||
"packages": [
|
||||
{
|
||||
"registryType": "oci",
|
||||
"identifier": "ghcr.io/safedep/vet:vVERSION_FROM_ENV",
|
||||
"runtimeHint": "docker",
|
||||
"runtimeArguments": [
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--rm",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"type": "named",
|
||||
"name": "-i",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"packageArguments": [
|
||||
{
|
||||
"type": "positional",
|
||||
"value": "-s"
|
||||
},
|
||||
{
|
||||
"type": "named",
|
||||
"name": "-l",
|
||||
"value": "/tmp/vet-mcp.log"
|
||||
},
|
||||
{
|
||||
"type": "positional",
|
||||
"value": "server"
|
||||
},
|
||||
{
|
||||
"type": "positional",
|
||||
"value": "mcp"
|
||||
}
|
||||
],
|
||||
"transport": {
|
||||
"type": "stdio"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
golang 1.23.2
|
||||
golang 1.25.1
|
||||
gitleaks 8.16.4
|
||||
|
||||
2
.vscode/settings.json
vendored
@ -3,7 +3,7 @@
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "golang.go",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"gopls": {
|
||||
|
||||
@ -25,35 +25,38 @@ Create a new issue and add the label "enhancement".
|
||||
|
||||
When contributing changes to repository, follow these steps:
|
||||
|
||||
1. Ensure tests are passing
|
||||
2. Ensure you write test cases for new code
|
||||
3. `Signed-off-by` line is required in commit message (use `-s` flag while committing)
|
||||
1. If you modified code that requires generation (e.g., enum registrations, ent schemas), run `make generate` and commit the generated files
|
||||
2. Ensure tests are passing
|
||||
3. Ensure you write test cases for new code
|
||||
4. `Signed-off-by` line is required in commit message (use `-s` flag while committing)
|
||||
|
||||
## Developer Setup
|
||||
|
||||
### Requirements
|
||||
|
||||
* Go 1.22+
|
||||
- Go 1.25.0+
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
* Install [ASDF](https://asdf-vm.com/)
|
||||
* Install the development tools
|
||||
- Install [ASDF](https://asdf-vm.com/)
|
||||
- Install the development tools
|
||||
|
||||
```bash
|
||||
asdf plugin add golang
|
||||
asdf plugin add gitleaks
|
||||
asdf install
|
||||
```
|
||||
|
||||
* Install `lefthook`
|
||||
- Install git hooks (using Go toolchain)
|
||||
|
||||
```bash
|
||||
go install github.com/evilmartians/lefthook@latest
|
||||
go tool github.com/evilmartians/lefthook install
|
||||
```
|
||||
|
||||
* Install git hooks
|
||||
Install `golangci-lint`
|
||||
|
||||
```bash
|
||||
$(go env GOPATH)/bin/lefthook install
|
||||
```shell
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
|
||||
```
|
||||
|
||||
### Build
|
||||
@ -76,11 +79,24 @@ Quick build without regenerating code from API specs
|
||||
make quick-vet
|
||||
```
|
||||
|
||||
### Generate Code
|
||||
|
||||
If you modify code that requires generation (enum registrations in `pkg/analyzer/filterv2/enums.go`, ent schemas in `ent/schema/*.go`), run:
|
||||
|
||||
```bash
|
||||
make generate
|
||||
```
|
||||
|
||||
**Important**: Generated files must be committed to the repository. CI will fail if generated code is out of sync.
|
||||
|
||||
### Format Code
|
||||
|
||||
```bash
|
||||
golangci-lint fmt
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
|
||||
```bash
|
||||
go test -v ./...
|
||||
make test
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
27
Dockerfile
@ -1,28 +1,45 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:1.23-bullseye AS build
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm@sha256:c4bc0741e3c79c0e2d47ca2505a06f5f2a44682ada94e1dba251a3854e60c2bd AS build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Install cross-compilation tools
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc-aarch64-linux-gnu \
|
||||
libc6-dev-arm64-cross \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
RUN make quick-vet
|
||||
# Set up cross-compilation environment based on target platform
|
||||
RUN case "${TARGETPLATFORM}" in \
|
||||
"linux/amd64") \
|
||||
CC=gcc CXX=g++ GOOS=linux GOARCH=amd64 make quick-vet ;; \
|
||||
"linux/arm64") \
|
||||
CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ GOOS=linux GOARCH=arm64 make quick-vet ;; \
|
||||
*) echo "Unsupported platform: ${TARGETPLATFORM}" && exit 1 ;; \
|
||||
esac
|
||||
|
||||
FROM gcr.io/distroless/cc
|
||||
FROM debian:12-slim@sha256:b1a741487078b369e78119849663d7f1a5341ef2768798f7b7406c4240f86aef
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/safedep/vet
|
||||
LABEL org.opencontainers.image.description="Open source software supply chain security tool"
|
||||
LABEL org.opencontainers.image.licenses=Apache-2.0
|
||||
LABEL io.modelcontextprotocol.server.name="io.github.safedep/vet-mcp"
|
||||
|
||||
COPY ./samples/ /vet/samples
|
||||
COPY --from=build /build/vet /usr/local/bin/vet
|
||||
|
||||
USER nonroot:nonroot
|
||||
|
||||
ENTRYPOINT ["vet"]
|
||||
|
||||
14
Makefile
@ -8,20 +8,16 @@ all: quick-vet
|
||||
ent:
|
||||
go generate ./ent
|
||||
|
||||
linter-install:
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.0
|
||||
.PHONY: filterv2-gen
|
||||
filterv2-gen:
|
||||
go generate ./pkg/analyzer/filterv2/...
|
||||
|
||||
oapi-codegen-install:
|
||||
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.10.1
|
||||
generate: ent filterv2-gen
|
||||
|
||||
protoc-install:
|
||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
|
||||
dev-setup: linter-install oapi-codegen-install protoc-install
|
||||
|
||||
oapi-codegen:
|
||||
oapi-codegen -package insightapi -generate types ./api/insights-v1.yml > ./gen/insightapi/insights.types.go
|
||||
oapi-codegen -package insightapi -generate client ./api/insights-v1.yml > ./gen/insightapi/insights.client.go
|
||||
dev-setup: protoc-install
|
||||
|
||||
protoc-codegen:
|
||||
protoc -I ./api \
|
||||
|
||||
696
README.md
@ -1,338 +1,582 @@
|
||||
<h1 align="center">
|
||||
<img alt="SafeDep Vet" src="docs/static/img/vet-logo.png" width="150" />
|
||||
</h1>
|
||||
<div align="center">
|
||||
<img width="3024" height="1964" alt="image" src="./docs/assets/vet-terminal.png" />
|
||||
|
||||
<p align="center">
|
||||
Created and maintained by <b><a href="https://safedep.io/">https://safedep.io</a></b> with contributions from the community 🚀
|
||||
</p>
|
||||
<h1>SafeDep VET</h1>
|
||||
|
||||
<p><strong>🚀 Enterprise grade open source software supply chain security</strong></p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/safedep/vet/releases"><strong>Download</strong></a> •
|
||||
<a href="#-quick-start"><strong>Quick Start</strong></a> •
|
||||
<a href="https://docs.safedep.io/"><strong>Documentation</strong></a> •
|
||||
<a href="#-community"><strong>Community</strong></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://goreportcard.com/report/github.com/safedep/vet)
|
||||

|
||||

|
||||
[](https://github.com/safedep/vet/blob/main/LICENSE)
|
||||
[](https://github.com/safedep/vet/releases)
|
||||
[](https://api.securityscorecards.dev/projects/github.com/safedep/vet)
|
||||
[](https://github.com/safedep/vet/actions/workflows/codeql.yml)
|
||||
[](https://slsa.dev)
|
||||
[](https://github.com/safedep/vet/actions/workflows/scorecard.yml)
|
||||
[](https://twitter.com/intent/follow?screen_name=safedepio)
|
||||
[](https://github.com/safedep/vet/actions/workflows/codeql.yml)
|
||||
[](https://pkg.go.dev/github.com/safedep/vet)
|
||||
|
||||
[](https://safedep.io/docs)
|
||||
[](https://deepwiki.com/safedep/vet)
|
||||
|
||||
## Policy as Code for Open Source Software Supply Chain
|
||||
</div>
|
||||
|
||||
`vet` is a tool for identifying risks in open source software supply chain. It
|
||||
goes beyond just vulnerabilities and provides visibility on OSS package risks
|
||||
due to it's license, popularity, security hygiene, and more. `vet` is designed
|
||||
with the goal of helping software development teams consume safe and trusted
|
||||
OSS components through automated vetting in CI/CD.
|
||||
---
|
||||
|
||||
* [🔥 vet in action](#-vet-in-action)
|
||||
* [Getting Started](#getting-started)
|
||||
* [Running Scan](#running-scan)
|
||||
* [Scanning Binary Artifacts](#scanning-binary-artifacts)
|
||||
* [Scanning SBOM](#scanning-sbom)
|
||||
* [Scanning Github Repositories](#scanning-github-repositories)
|
||||
* [Scanning Github Organization](#scanning-github-organization)
|
||||
* [Scanning Package URL](#scanning-package-url)
|
||||
* [Available Parsers](#available-parsers)
|
||||
* [Policy as Code](#policy-as-code)
|
||||
* [Query Mode](#query-mode)
|
||||
* [Reporting](#reporting)
|
||||
* [CI/CD Integration](#ci/cd-integration)
|
||||
* [📦 GitHub Action](#-github-action)
|
||||
* [🚀 GitLab CI](#-gitlab-ci)
|
||||
* [🐙 Malicious Package Analysis](#-malicious-package-analysis)
|
||||
* [🛠️ Advanced Usage](#-advanced-usage)
|
||||
* [📖 Documentation](#-documentation)
|
||||
* [🎊 Community](#-community)
|
||||
* [💻 Development](#-development)
|
||||
* [Support](#support)
|
||||
* [Star History](#star-history)
|
||||
* [🔖 References](#-references)
|
||||
## 🎯 Why vet?
|
||||
|
||||
## 🔥 vet in action
|
||||
> **70-90% of modern software constitute code from open sources** — How do we know if it's safe?
|
||||
|
||||

|
||||
**vet** is an open source software supply chain security tool built for **developers and security engineers** who need:
|
||||
|
||||
## Getting Started
|
||||
✅ **Next-gen Software Composition Analysis** — Vulnerability and malicious package detection
|
||||
✅ **Policy as Code** — Express opinionated security policies using [CEL](https://cel.dev/)
|
||||
✅ **Real-time malicious package detection** — Powered by [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis) active scanning
|
||||
✅ **Multi-ecosystem support** — npm, PyPI, Maven, Go, Docker, GitHub Actions, and more
|
||||
✅ **CI/CD native** — Built for DevSecOps workflows with support for GitHub Actions, GitLab CI, and more
|
||||
✅ **MCP Server** — Run `vet` as a MCP server to vet open source packages from AI suggested code
|
||||
✅ **Agents** — Run AI agents to query and analyze scan results
|
||||
|
||||
- Download the binary file for your operating system / architecture from the [Official GitHub Releases](https://github.com/safedep/vet/releases)
|
||||
## ⚡ Quick Start
|
||||
|
||||
- You can also install `vet` using homebrew in MacOS and Linux
|
||||
**Install in seconds:**
|
||||
|
||||
```bash
|
||||
# macOS & Linux
|
||||
brew install safedep/tap/vet
|
||||
```
|
||||
|
||||
or download a [pre-built binary](https://github.com/safedep/vet/releases)
|
||||
|
||||
**Scan your project:**
|
||||
|
||||
```bash
|
||||
# Scan current directory
|
||||
vet scan -D .
|
||||
|
||||
# Scan a single file
|
||||
vet scan -M package-lock.json
|
||||
|
||||
# Fail CI on critical vulnerabilities
|
||||
vet scan -D . --filter 'vulns.critical.exists(p, true)' --filter-fail
|
||||
|
||||
# Fail CI on OpenSSF Scorecard requirements
|
||||
vet scan -D . --filter 'scorecard.scores.Maintained < 5' --filter-fail
|
||||
|
||||
# Fail CI if a package is published from a GitHub repository with less than 5 stars
|
||||
vet scan -D . --filter 'projects.exists(p, p.type == "GITHUB" && p.stars < 5)' --filter-fail
|
||||
```
|
||||
|
||||
## 🔒 Key Features
|
||||
|
||||
### 🕵️ **Code Analysis**
|
||||
|
||||
Unlike dependency scanners that flood you with noise, `vet` analyzes your **actual code usage** to prioritize real risks. See [dependency usage evidence](https://docs.safedep.io/guides/dependency-usage-identification) for more details.
|
||||
|
||||
### 🛡️ **Malicious Package Detection**
|
||||
|
||||
Integrated with [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis) for real-time protection against malicious packages in the wild. Free for open source projects. Fallback to _Query Mode_ when API key is not provided. Read more [about malicious package scanning](#️-malicious-package-detection-1).
|
||||
|
||||
### 📋 **Policy as Code**
|
||||
|
||||
Define security policies using CEL expressions to enforce context specific security requirements.
|
||||
|
||||
```bash
|
||||
# Block packages with critical CVEs
|
||||
vet scan \
|
||||
--filter 'vulns.critical.exists(p, true)'
|
||||
|
||||
# Enforce license compliance
|
||||
vet scan \
|
||||
--filter 'licenses.contains_license("GPL-3.0")'
|
||||
|
||||
# Enforce OpenSSF Scorecard requirements
|
||||
# Require minimum OpenSSF Scorecard scores
|
||||
vet scan \
|
||||
--filter 'scorecard.scores.Maintained < 5'
|
||||
```
|
||||
|
||||
### 🎯 **Multi-Format Support**
|
||||
|
||||
- **Package Managers**: npm, PyPI, Maven, Go, Ruby, Rust, PHP
|
||||
- **Container Images**: Docker, OCI
|
||||
- **SBOMs**: CycloneDX, SPDX
|
||||
- **Binary Artifacts**: JAR files, Python wheels
|
||||
- **Source Code**: Direct repository scanning
|
||||
|
||||
## 🔥 See vet in Action
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/assets/vet-demo.gif" alt="vet Demo" width="100%" />
|
||||
</div>
|
||||
|
||||
## 🚀 Production Ready Integrations
|
||||
|
||||
### 📦 **GitHub Actions**
|
||||
|
||||
Zero config security guardrails against vulnerabilities and malicious packages in your CI/CD pipeline
|
||||
**with your own opinionated policies**:
|
||||
|
||||
```yaml
|
||||
- uses: safedep/vet-action@v1
|
||||
with:
|
||||
policy: ".github/vet/policy.yml"
|
||||
```
|
||||
|
||||
See more in [vet-action](https://github.com/safedep/vet-action) documentation.
|
||||
|
||||
### 🔧 **GitLab CI**
|
||||
|
||||
Enterprise grade scanning with [vet CI Component](https://gitlab.com/explore/catalog/safedep/ci-components/vet):
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- component: gitlab.com/safedep/ci-components/vet/scan@main
|
||||
```
|
||||
|
||||
### 🐳 **Container Integration**
|
||||
|
||||
Run `vet` anywhere, even your internal developer platform or custom CI/CD environment using our container image.
|
||||
|
||||
```bash
|
||||
docker run --rm -v $(pwd):/app ghcr.io/safedep/vet:latest scan -D /app
|
||||
```
|
||||
|
||||
## 📚 Table of Contents
|
||||
|
||||
- [🎯 Why vet?](#-why-vet)
|
||||
- [⚡ Quick Start](#-quick-start)
|
||||
- [🔒 Key Features](#-key-features)
|
||||
- [🕵️ **Code Analysis**](#️-code-analysis)
|
||||
- [🛡️ **Malicious Package Detection**](#️-malicious-package-detection)
|
||||
- [📋 **Policy as Code**](#-policy-as-code)
|
||||
- [🎯 **Multi-Format Support**](#-multi-format-support)
|
||||
- [🔥 See vet in Action](#-see-vet-in-action)
|
||||
- [🚀 Production Ready Integrations](#-production-ready-integrations)
|
||||
- [📦 **GitHub Actions**](#-github-actions)
|
||||
- [🔧 **GitLab CI**](#-gitlab-ci)
|
||||
- [🐳 **Container Integration**](#-container-integration)
|
||||
- [📚 Table of Contents](#-table-of-contents)
|
||||
- [📦 Installation Options](#-installation-options)
|
||||
- [🍺 **Homebrew (Recommended)**](#-homebrew-recommended)
|
||||
- [📥 **Direct Download**](#-direct-download)
|
||||
- [🐹 **Go Install**](#-go-install)
|
||||
- [🐳 **Container Image**](#-container-image)
|
||||
- [⚙️ **Verify Installation**](#️-verify-installation)
|
||||
- [🎮 Advanced Usage](#-advanced-usage)
|
||||
- [🔍 **Scanning Options**](#-scanning-options)
|
||||
- [🎯 **Policy Enforcement Examples**](#-policy-enforcement-examples)
|
||||
- [🔧 **SBOM Support**](#-sbom-support)
|
||||
- [📊 **Query Mode \& Data Persistence**](#-query-mode--data-persistence)
|
||||
- [📊 Reporting](#-reporting)
|
||||
- [📋 **Report Formats**](#-report-formats)
|
||||
- [🎯 **Report Examples**](#-report-examples)
|
||||
- [🤖 **MCP Server**](#-mcp-server)
|
||||
- [🤖 **Agents**](#-agents)
|
||||
- [🛡️ Malicious Package Detection](#️-malicious-package-detection-1)
|
||||
- [🚀 **Quick Setup**](#-quick-setup)
|
||||
- [🎯 **Advanced Malicious Package Analysis**](#-advanced-malicious-package-analysis)
|
||||
- [🔒 **Security Features**](#-security-features)
|
||||
- [📊 Privacy and Telemetry](#-privacy-and-telemetry)
|
||||
- [🎊 Community \& Support](#-community--support)
|
||||
- [🌟 **Join the Community**](#-join-the-community)
|
||||
- [💡 **Get Help \& Share Ideas**](#-get-help--share-ideas)
|
||||
- [⭐ **Star History**](#-star-history)
|
||||
- [🙏 **Built With Open Source**](#-built-with-open-source)
|
||||
|
||||
## 📦 Installation Options
|
||||
|
||||
### 🍺 **Homebrew (Recommended)**
|
||||
|
||||
```bash
|
||||
brew tap safedep/tap
|
||||
brew install safedep/tap/vet
|
||||
```
|
||||
|
||||
- Alternatively, build from source
|
||||
### 📥 **Direct Download**
|
||||
|
||||
> Ensure $(go env GOPATH)/bin is in your $PATH
|
||||
See [releases](https://github.com/safedep/vet/releases) for the latest version.
|
||||
|
||||
### 🐹 **Go Install**
|
||||
|
||||
```bash
|
||||
go install github.com/safedep/vet@latest
|
||||
```
|
||||
|
||||
- Also available as a container image
|
||||
### 🐳 **Container Image**
|
||||
|
||||
```bash
|
||||
docker run --rm -it ghcr.io/safedep/vet:latest version
|
||||
# Quick test
|
||||
docker run --rm ghcr.io/safedep/vet:latest version
|
||||
|
||||
# Scan local directory
|
||||
docker run --rm -v $(pwd):/workspace ghcr.io/safedep/vet:latest scan -D /workspace
|
||||
```
|
||||
|
||||
> **Note:** Container image is built for x86_64 Linux only. Use a
|
||||
> [pre-built binary](https://github.com/safedep/vet/releases) or
|
||||
> build from source for other platforms.
|
||||
|
||||
### Running Scan
|
||||
|
||||
- Run `vet` to identify risks by scanning a directory
|
||||
### ⚙️ **Verify Installation**
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/repository
|
||||
vet version
|
||||
# Should display version and build information
|
||||
```
|
||||
|
||||

|
||||
## 🎮 Advanced Usage
|
||||
|
||||
- Run `vet` to scan specific (supported) package manifests
|
||||
### 🔍 **Scanning Options**
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
**📁 Directory Scanning**
|
||||
|
||||
```bash
|
||||
vet scan -M /path/to/pom.xml
|
||||
vet scan -M /path/to/requirements.txt
|
||||
vet scan -M /path/to/package-lock.json
|
||||
# Scan current directory
|
||||
vet scan
|
||||
|
||||
# Scan a given directory
|
||||
vet scan -D /path/to/project
|
||||
|
||||
# Resolve and scan transitive dependencies
|
||||
vet scan -D . --transitive
|
||||
```
|
||||
|
||||
**Note:** `--lockfiles` is generalized to `-M` or `--manifests` to support additional
|
||||
types of package manifests or other artifacts in future.
|
||||
|
||||
#### Scanning Binary Artifacts
|
||||
|
||||
- Scan a Java JAR file
|
||||
**📄 Manifest Files**
|
||||
|
||||
```bash
|
||||
vet scan -M /path/to/app.jar
|
||||
# Package managers
|
||||
vet scan -M package-lock.json
|
||||
vet scan -M requirements.txt
|
||||
vet scan -M pom.xml
|
||||
vet scan -M go.mod
|
||||
vet scan -M Gemfile.lock
|
||||
```
|
||||
|
||||
> Suitable for scanning bootable JARs with embedded dependencies
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
- Scan a directory with JAR files
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/jars --type jar
|
||||
```
|
||||
|
||||
#### Scanning SBOM
|
||||
|
||||
- Scan an SBOM in [CycloneDX](https://cyclonedx.org/) format
|
||||
|
||||
```bash
|
||||
vet scan -M /path/to/cyclonedx-sbom.json --type bom-cyclonedx
|
||||
```
|
||||
|
||||
- Scan an SBOM in [SPDX](https://spdx.dev/) format
|
||||
|
||||
```bash
|
||||
vet scan -M /path/to/spdx-sbom.json --type bom-spdx
|
||||
```
|
||||
|
||||
**Note:** `--type` is a generalized version of `--lockfile-as` to support additional
|
||||
artifact types in future.
|
||||
|
||||
> **Note:** SBOM scanning feature is currently in experimental stage
|
||||
|
||||
#### Scanning Github Repositories
|
||||
|
||||
- Setup github access token to scan private repo
|
||||
**🐙 GitHub Integration**
|
||||
|
||||
```bash
|
||||
# Setup GitHub access
|
||||
vet connect github
|
||||
|
||||
# Scan repositories
|
||||
vet scan --github https://github.com/user/repo
|
||||
|
||||
# Organization scanning
|
||||
vet scan --github-org https://github.com/org
|
||||
```
|
||||
|
||||
Alternatively, set `GITHUB_TOKEN` environment variable with [Github PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
|
||||
|
||||
- To scan remote Github repositories, including private ones
|
||||
**📦 Artifact Scanning**
|
||||
|
||||
```bash
|
||||
vet scan --github https://github.com/safedep/vet
|
||||
# Container images
|
||||
vet scan --image nginx:latest
|
||||
vet scan --image /path/to/image-saved-file.tar
|
||||
|
||||
# Binary artifacts
|
||||
vet scan -M app.jar
|
||||
vet scan -M package.whl
|
||||
```
|
||||
|
||||
**Note:** You may need to enable [Dependency Graph](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-the-dependency-graph) at repository or organization level for Github repository scanning to work.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### Scanning Github Organization
|
||||
|
||||
> You must setup the required access for scanning private repositories
|
||||
> before scanning organizations
|
||||
### 🎯 **Policy Enforcement Examples**
|
||||
|
||||
```bash
|
||||
vet scan --github-org https://github.com/safedep
|
||||
```
|
||||
|
||||
> **Note:** `vet` will block and wait if it encounters Github secondary rate limit.
|
||||
|
||||
#### Scanning Package URL
|
||||
|
||||
- To scan a [purl](https://github.com/package-url/purl-spec)
|
||||
|
||||
```bash
|
||||
vet scan --purl pkg:/gem/nokogiri@1.10.4
|
||||
```
|
||||
|
||||
#### Available Parsers
|
||||
|
||||
- List supported package manifest parsers including experimental modules
|
||||
|
||||
```bash
|
||||
vet scan parsers --experimental
|
||||
```
|
||||
|
||||
## Policy as Code
|
||||
|
||||
`vet` uses [Common Expressions Language](https://github.com/google/cel-spec)
|
||||
(CEL) as the policy language. Policies can be defined to build guardrails
|
||||
preventing introduction of insecure components.
|
||||
|
||||
### Vulnerability
|
||||
|
||||
- Run `vet` and fail if a critical or high vulnerability was detected
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/code \
|
||||
# Security-first scanning
|
||||
vet scan -D . \
|
||||
--filter 'vulns.critical.exists(p, true) || vulns.high.exists(p, true)' \
|
||||
--filter-fail
|
||||
```
|
||||
|
||||
### License
|
||||
# License compliance
|
||||
vet scan -D . \
|
||||
--filter 'licenses.contains_license("GPL-3.0")' \
|
||||
--filter-fail
|
||||
|
||||
- Run `vet` and fail if a package with a specific license was detected
|
||||
# OpenSSF Scorecard requirements
|
||||
vet scan -D . \
|
||||
--filter 'scorecard.scores.Maintained < 5' \
|
||||
--filter-fail
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/code \
|
||||
--filter 'licenses.exists(p, "GPL-2.0")' \
|
||||
# Popularity-based filtering
|
||||
vet scan -D . \
|
||||
--filter 'projects.exists(p, p.type == "GITHUB" && p.stars < 50)' \
|
||||
--filter-fail
|
||||
```
|
||||
|
||||
**Note:** Using `licenses.contains_license(...)` is recommended for license matching due
|
||||
to its support for SPDX expressions.
|
||||
|
||||
- `vet` supports [SPDX License Expressions](https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/) at package license and policy level
|
||||
### 🔧 **SBOM Support**
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/code \
|
||||
--filter 'licenses.contains_license("LGPL-2.1+")' \
|
||||
--filter-fail
|
||||
# Scan a CycloneDX SBOM
|
||||
vet scan -M sbom.json --type bom-cyclonedx
|
||||
|
||||
# Scan a SPDX SBOM
|
||||
vet scan -M sbom.spdx.json --type bom-spdx
|
||||
|
||||
# Generate SBOM output
|
||||
vet scan -D . --report-cdx=output.sbom.json
|
||||
|
||||
# Package URL scanning
|
||||
vet scan --purl pkg:npm/lodash@4.17.21
|
||||
```
|
||||
|
||||
### Scorecard
|
||||
### 📊 **Query Mode & Data Persistence**
|
||||
|
||||
- Run `vet` and fail based on [OpenSSF Scorecard](https://securityscorecards.dev/) attributes
|
||||
For large codebases and repeated analysis:
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/code \
|
||||
--filter 'scorecard.scores.Maintained == 0' \
|
||||
--filter-fail
|
||||
# Scan once, query multiple times
|
||||
vet scan -D . --json-dump-dir ./scan-data
|
||||
|
||||
# Query with different filters
|
||||
vet query --from ./scan-data \
|
||||
--filter 'vulns.critical.exists(p, true)'
|
||||
|
||||
# Generate focused reports
|
||||
vet query --from ./scan-data \
|
||||
--filter 'licenses.contains_license("GPL")' \
|
||||
--report-json license-violations.json
|
||||
```
|
||||
|
||||
For more examples, refer to [documentation](https://docs.safedep.io/advanced/policy-as-code)
|
||||
## 📊 Reporting
|
||||
|
||||
## Query Mode
|
||||
**vet** generate reports that are tailored for different stakeholders:
|
||||
|
||||
- Run scan and dump internal data structures to a file for further querying
|
||||
### 📋 **Report Formats**
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="30%"><strong>🔍 For Security Teams</strong></td>
|
||||
<td width="70%">
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/code --json-dump-dir /path/to/dump
|
||||
# SARIF for GitHub Security tab
|
||||
vet scan -D . --report-sarif=report.sarif
|
||||
|
||||
# JSON for custom tooling
|
||||
vet scan -D . --report-json=report.json
|
||||
|
||||
# CSV for spreadsheet analysis
|
||||
vet scan -D . --report-csv=report.csv
|
||||
|
||||
# HTML for web-based analysis
|
||||
vet scan -D . --report-html=report.html
|
||||
```
|
||||
|
||||
- Filter results using `query` command
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>📖 For Developers</strong></td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
vet query --from /path/to/dump \
|
||||
--filter 'vulns.critical.exists(p, true) || vulns.high.exists(p, true)'
|
||||
# Markdown reports for PRs
|
||||
vet scan -D . --report-markdown=report.md
|
||||
|
||||
# Console summary (default)
|
||||
vet scan -D . --report-summary
|
||||
```
|
||||
|
||||
- Generate report from dumped data
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>🏢 For Compliance</strong></td>
|
||||
<td>
|
||||
|
||||
```bash
|
||||
vet query --from /path/to/dump --report-json /path/to/report.json
|
||||
# SBOM generation
|
||||
vet scan -D . --report-cdx=sbom.json
|
||||
|
||||
# Dependency graphs
|
||||
vet scan -D . --report-graph=dependencies.dot
|
||||
```
|
||||
|
||||
## Reporting
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
`vet` supports generating reports in multiple formats during `scan` or `query`
|
||||
execution.
|
||||
|
||||
| Format | Description |
|
||||
|----------|--------------------------------------------------------------------------------|
|
||||
| Markdown | Human readable report for vulnerabilities, licenses, and more |
|
||||
| CSV | Export data to CSV format for manual slicing and dicing |
|
||||
| JSON | Machine readable JSON format following internal schema (maximum data) |
|
||||
| SARIF | Useful for integration with Github Code Scanning and other tools |
|
||||
| Graph | Dependency graph in DOT format for risk and package relationship visualization |
|
||||
| Summary | Default console report with summary of vulnerabilities, licenses, and more |
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### 📦 GitHub Action
|
||||
|
||||
- `vet` is available as a GitHub Action, refer to [vet-action](https://github.com/safedep/vet-action)
|
||||
|
||||
### 🚀 GitLab CI
|
||||
|
||||
- `vet` can be integrated with GitLab CI, refer to [vet-gitlab-ci](https://docs.safedep.io/integrations/gitlab-ci)
|
||||
|
||||
## 🐙 Malicious Package Analysis
|
||||
|
||||
`vet` supports scanning for malicious packages using [SafeDep Cloud API](https://docs.safedep.io/cloud/malware-analysis)
|
||||
|
||||
- Run a scan and check for malicious packages
|
||||
### 🎯 **Report Examples**
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/code --malware
|
||||
# Multi-format output
|
||||
vet scan -D . \
|
||||
--report-json=report.json \
|
||||
--report-sarif=report.sarif \
|
||||
--report-markdown=report.md \
|
||||
--report-html=report.html
|
||||
|
||||
# Focus on specific issues
|
||||
vet scan -D . \
|
||||
--filter 'vulns.high.exists(p, true)' \
|
||||
--report-json=report.json
|
||||
```
|
||||
|
||||
**Note**: `vet` will submit identified packages to SafeDep Cloud for analysis and wait
|
||||
for a `timeout` period for response. Not all package analysis may be completed
|
||||
within the timeout period. However, subsequent scans will fetch the results if
|
||||
available and lead to increased coverage over time. Adjust the timeout using
|
||||
`--malware-analysis-timeout` flag.
|
||||
### 🤖 **MCP Server**
|
||||
|
||||
## 🛠️ Advanced Usage
|
||||
**vet** can be used as an MCP server to vet open source packages from AI suggested code.
|
||||
|
||||
- [Threat Hunting with vet](https://docs.safedep.io/advanced/filtering)
|
||||
- [Policy as Code](https://docs.safedep.io/advanced/policy-as-code)
|
||||
- [Exceptions and Overrides](https://docs.safedep.io/advanced/exceptions)
|
||||
```bash
|
||||
# Start the MCP server with SSE transport
|
||||
vet server mcp --server-type sse
|
||||
```
|
||||
|
||||
## 📖 Documentation
|
||||
For more details, see [vet MCP Server](./docs/mcp.md) documentation.
|
||||
|
||||
- Refer to [https://safedep.io/docs](https://safedep.io/docs) for the detailed documentation
|
||||
### 🤖 **Agents**
|
||||
|
||||
[](https://safedep.io/docs)
|
||||
See [vet Agents](./docs/agent.md) documentation for more details.
|
||||
|
||||
## 🎊 Community
|
||||
## 🛡️ Malicious Package Detection
|
||||
|
||||
First of all, thank you so much for showing interest in `vet`, we appreciate it ❤️
|
||||
**Malicious package detection through active scanning and code analysis** powered by
|
||||
[SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis). `vet` requires an API
|
||||
key for active scanning of unknown packages. When API key is not provided, `vet` will
|
||||
fallback to _Query Mode_ which detects known malicious packages from [SafeDep](https://safedep.io)
|
||||
and [OSV](https://osv.dev) databases.
|
||||
|
||||
- Join the Discord server using the link - [https://rebrand.ly/safedep-community](https://rebrand.ly/safedep-community)
|
||||
- Grab a free API key by running `vet cloud quickstart`
|
||||
- API access is free forever for open source projects
|
||||
- No proprietary code is collected for malicious package detection
|
||||
- Only open source package scanning from public repositories is supported
|
||||
|
||||
[](https://rebrand.ly/safedep-community)
|
||||
### 🚀 **Quick Setup**
|
||||
|
||||
## 💻 Development
|
||||
> Malicious package detection requires an API key for [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis).
|
||||
|
||||
Refer to [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
```bash
|
||||
# One-time setup
|
||||
vet cloud quickstart
|
||||
|
||||
## Support
|
||||
# Enable malware scanning
|
||||
vet scan -D . --malware
|
||||
|
||||
[SafeDep](https://safedep.io) provides enterprise support for `vet`
|
||||
deployments. Check out [SafeDep Cloud](https://safedep.io) for large scale
|
||||
deployment and management of `vet` in your organization.
|
||||
# Query for known malicious packages without API key
|
||||
vet scan -D . --malware-query
|
||||
```
|
||||
|
||||
## Star History
|
||||
Example malicious packages detected and reported by [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis)
|
||||
malicious package detection:
|
||||
|
||||
- [MAL-2025-3541: express-cookie-parser](https://safedep.io/malicious-npm-package-express-cookie-parser/)
|
||||
- [MAL-2025-4339: eslint-config-airbnb-compat](https://safedep.io/digging-into-dynamic-malware-analysis-signals/)
|
||||
- [MAL-2025-4029: ts-runtime-compat-check](https://safedep.io/digging-into-dynamic-malware-analysis-signals/)
|
||||
- [MAL-2025-2227: nyc-config](https://safedep.io/nyc-config-malicious-package/)
|
||||
|
||||
### 🎯 **Advanced Malicious Package Analysis**
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%">
|
||||
|
||||
**🔍 Scan packages with malicious package detection enabled**
|
||||
|
||||
```bash
|
||||
# Real-time scanning
|
||||
vet scan -D . --malware
|
||||
|
||||
# Timeout adjustment
|
||||
vet scan -D . --malware \
|
||||
--malware-analysis-timeout=300s
|
||||
|
||||
# Batch analysis
|
||||
vet scan -D . --malware \
|
||||
--json-dump-dir=./analysis
|
||||
```
|
||||
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
**🎭 Specialized Scans**
|
||||
|
||||
```bash
|
||||
# VS Code extensions
|
||||
vet scan --vsx --malware
|
||||
|
||||
# GitHub Actions
|
||||
vet scan -D .github/workflows --malware
|
||||
|
||||
# Container Images
|
||||
vet scan --image nats:2.10 --malware
|
||||
|
||||
# Scan a single package and fail if its malicious
|
||||
vet scan --purl pkg:/npm/nyc-config@10.0.0 --fail-fast
|
||||
|
||||
# Active scanning of a single package (requires API key)
|
||||
vet inspect malware \
|
||||
--purl pkg:npm/nyc-config@10.0.0
|
||||
```
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### 🔒 **Security Features**
|
||||
|
||||
- ✅ **Real-time analysis** of packages against known malware databases
|
||||
- ✅ **Behavioral analysis** using static and dynamic analysis
|
||||
- ✅ **Zero day protection** through active code scanning
|
||||
- ✅ **Human in the loop** for triaging and investigation of high impact findings
|
||||
- ✅ **Real time analysis** with public [analysis log](https://vetpkg.dev/mal)
|
||||
|
||||
## 📊 Privacy and Telemetry
|
||||
|
||||
`vet` collects anonymous usage telemetry to improve the product. **Your code and package information is never transmitted.**
|
||||
|
||||
```bash
|
||||
# Disable telemetry (optional)
|
||||
export VET_DISABLE_TELEMETRY=true
|
||||
```
|
||||
|
||||
## 🎊 Community & Support
|
||||
|
||||
<div align="center">
|
||||
|
||||
### 🌟 **Join the Community**
|
||||
|
||||
[](https://rebrand.ly/safedep-community)
|
||||
[](https://github.com/safedep/vet/discussions)
|
||||
[](https://twitter.com/safedepio)
|
||||
|
||||
</div>
|
||||
|
||||
### 💡 **Get Help & Share Ideas**
|
||||
|
||||
- 🚀 **[Interactive Tutorial](https://killercoda.com/safedep/scenario/101-intro)** - Learn vet hands-on
|
||||
- 📚 **[Complete Documentation](https://docs.safedep.io/)** - Comprehensive guides
|
||||
- 💬 **[Discord Community](https://rebrand.ly/safedep-community)** - Real-time support
|
||||
- 🐛 **[Issue Tracker](https://github.com/safedep/vet/issues)** - Bug reports & feature requests
|
||||
- 🤝 **[Contributing Guide](CONTRIBUTING.md)** - Join the development
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
### ⭐ **Star History**
|
||||
|
||||
[](https://star-history.com/#safedep/vet&Date)
|
||||
|
||||
## 🔖 References
|
||||
### 🙏 **Built With Open Source**
|
||||
|
||||
- https://github.com/google/osv-scanner
|
||||
- https://github.com/anchore/syft
|
||||
- https://deps.dev/
|
||||
- https://securityscorecards.dev/
|
||||
- https://slsa.dev/
|
||||
vet stands on the shoulders of giants:
|
||||
|
||||
[OSV](https://osv.dev) • [OpenSSF Scorecard](https://securityscorecards.dev/) • [SLSA](https://slsa.dev/) • [OSV-SCALIBR](https://github.com/google/osv-scalibr) • [Syft](https://github.com/anchore/syft)
|
||||
|
||||
---
|
||||
|
||||
<p><strong>⚡ Secure your supply chain today. Star the repo ⭐ and get started!</strong></p>
|
||||
|
||||
Created with ❤️ by [SafeDep](https://safedep.io) and the open source community
|
||||
|
||||
</div>
|
||||
|
||||
<img referrerpolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=304d1856-fcb3-4166-bfbf-b3e40d0f1e3b" />
|
||||
|
||||
69
agent/agent.go
Normal file
@ -0,0 +1,69 @@
|
||||
// Package agent declares the building blocks for implement vet agent.
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
type Input struct {
|
||||
Query string
|
||||
}
|
||||
|
||||
type AnswerFormat string
|
||||
|
||||
const (
|
||||
AnswerFormatMarkdown AnswerFormat = "markdown"
|
||||
AnswerFormatJSON AnswerFormat = "json"
|
||||
)
|
||||
|
||||
type Output struct {
|
||||
Answer string
|
||||
Format AnswerFormat
|
||||
}
|
||||
|
||||
type Memory interface {
|
||||
AddInteraction(ctx context.Context, interaction *schema.Message) error
|
||||
GetInteractions(ctx context.Context) ([]*schema.Message, error)
|
||||
Clear(ctx context.Context) error
|
||||
}
|
||||
|
||||
type Session interface {
|
||||
ID() string
|
||||
Memory() Memory
|
||||
}
|
||||
|
||||
// AgentExecutionContext is to pass additional context to the agent
|
||||
// on a per execution basis. This is required so that an agent can be configured
|
||||
// and shared with different components while allowing the component to pass
|
||||
// additional context to the agent.
|
||||
type AgentExecutionContext struct {
|
||||
// OnToolCall is called when the agent is about to call a tool.
|
||||
// This is used for introspection only and not to mutate the agent's behavior.
|
||||
OnToolCall func(context.Context, Session, Input, string, string) error
|
||||
}
|
||||
|
||||
type AgentExecutionContextOpt func(*AgentExecutionContext)
|
||||
|
||||
func WithToolCallHook(fn func(context.Context, Session, Input, string, string) error) AgentExecutionContextOpt {
|
||||
return func(a *AgentExecutionContext) {
|
||||
a.OnToolCall = fn
|
||||
}
|
||||
}
|
||||
|
||||
type Agent interface {
|
||||
// Execute executes the agent with the given input and returns the output.
|
||||
// Internally the agent may perform a multi-step operation based on config,
|
||||
// instructions and available tools.
|
||||
Execute(context.Context, Session, Input, ...AgentExecutionContextOpt) (Output, error)
|
||||
}
|
||||
|
||||
// AgentToolCallIntrospectionFn is a function that introspects a tool call.
|
||||
// This is aligned with eino contract.
|
||||
type AgentToolCallIntrospectionFn func(context.Context /* name */, string /* args */, string) ( /* args */ string, error)
|
||||
|
||||
type ToolBuilder interface {
|
||||
Build(context.Context) ([]tool.BaseTool, error)
|
||||
}
|
||||
165
agent/llm.go
Normal file
@ -0,0 +1,165 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/model/claude"
|
||||
"github.com/cloudwego/eino-ext/components/model/gemini"
|
||||
"github.com/cloudwego/eino-ext/components/model/openai"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"google.golang.org/genai"
|
||||
)
|
||||
|
||||
// Map of fast vs. default models.
|
||||
var defaultModelMap = map[string]map[string]string{
|
||||
"openai": {
|
||||
"default": "gpt-4o",
|
||||
"fast": "gpt-4o-mini",
|
||||
},
|
||||
"claude": {
|
||||
"default": "claude-sonnet-4-20250514",
|
||||
"fast": "claude-sonnet-4-20250514",
|
||||
},
|
||||
"gemini": {
|
||||
"default": "gemini-2.5-pro",
|
||||
"fast": "gemini-2.5-flash",
|
||||
},
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Vendor string
|
||||
Name string
|
||||
Fast bool
|
||||
Client model.ToolCallingChatModel
|
||||
}
|
||||
|
||||
// BuildModelFromEnvironment builds a model from the environment variables.
|
||||
// The order of preference is:
|
||||
// 1. OpenAI
|
||||
// 2. Claude
|
||||
// 3. Gemini
|
||||
// 4. Others..
|
||||
func BuildModelFromEnvironment(fastMode bool) (*Model, error) {
|
||||
if model, err := buildOpenAIModelFromEnvironment(fastMode); err == nil {
|
||||
return model, nil
|
||||
}
|
||||
|
||||
if model, err := buildClaudeModelFromEnvironment(fastMode); err == nil {
|
||||
return model, nil
|
||||
}
|
||||
|
||||
if model, err := buildGeminiModelFromEnvironment(fastMode); err == nil {
|
||||
return model, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no usable LLM found for use with agent")
|
||||
}
|
||||
|
||||
func buildOpenAIModelFromEnvironment(fastMode bool) (*Model, error) {
|
||||
defaultModel := defaultModelMap["openai"]["default"]
|
||||
if fastMode {
|
||||
defaultModel = defaultModelMap["openai"]["fast"]
|
||||
}
|
||||
|
||||
modelName := os.Getenv("OPENAI_MODEL_OVERRIDE")
|
||||
if modelName == "" {
|
||||
modelName = defaultModel
|
||||
}
|
||||
|
||||
apiKey := os.Getenv("OPENAI_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("OPENAI_API_KEY is not set")
|
||||
}
|
||||
|
||||
model, err := openai.NewChatModel(context.Background(), &openai.ChatModelConfig{
|
||||
Model: modelName,
|
||||
APIKey: apiKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create openai model: %w", err)
|
||||
}
|
||||
|
||||
return &Model{
|
||||
Vendor: "openai",
|
||||
Name: modelName,
|
||||
Fast: fastMode,
|
||||
Client: model,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildClaudeModelFromEnvironment(fastMode bool) (*Model, error) {
|
||||
defaultModel := defaultModelMap["claude"]["default"]
|
||||
if fastMode {
|
||||
defaultModel = defaultModelMap["claude"]["fast"]
|
||||
}
|
||||
|
||||
modelName := os.Getenv("ANTHROPIC_MODEL_OVERRIDE")
|
||||
if modelName == "" {
|
||||
modelName = defaultModel
|
||||
}
|
||||
|
||||
apiKey := os.Getenv("ANTHROPIC_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("ANTHROPIC_API_KEY is not set")
|
||||
}
|
||||
|
||||
model, err := claude.NewChatModel(context.Background(), &claude.Config{
|
||||
Model: modelName,
|
||||
APIKey: apiKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create claude model: %w", err)
|
||||
}
|
||||
|
||||
return &Model{
|
||||
Vendor: "claude",
|
||||
Name: modelName,
|
||||
Fast: fastMode,
|
||||
Client: model,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildGeminiModelFromEnvironment(fastMode bool) (*Model, error) {
|
||||
defaultModel := defaultModelMap["gemini"]["default"]
|
||||
if fastMode {
|
||||
defaultModel = defaultModelMap["gemini"]["fast"]
|
||||
}
|
||||
|
||||
modelName := os.Getenv("GEMINI_MODEL_OVERRIDE")
|
||||
if modelName == "" {
|
||||
modelName = defaultModel
|
||||
}
|
||||
|
||||
apiKey := os.Getenv("GEMINI_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("GEMINI_API_KEY is not set")
|
||||
}
|
||||
|
||||
client, err := genai.NewClient(context.Background(), &genai.ClientConfig{
|
||||
APIKey: apiKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gemini client: %w", err)
|
||||
}
|
||||
|
||||
model, err := gemini.NewChatModel(context.Background(), &gemini.Config{
|
||||
Model: modelName,
|
||||
Client: client,
|
||||
ThinkingConfig: &genai.ThinkingConfig{
|
||||
IncludeThoughts: false,
|
||||
ThinkingBudget: nil,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gemini model: %w", err)
|
||||
}
|
||||
|
||||
return &Model{
|
||||
Vendor: "gemini",
|
||||
Name: modelName,
|
||||
Fast: fastMode,
|
||||
Client: model,
|
||||
}, nil
|
||||
}
|
||||
17
agent/llm_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDefaultModelsMap(t *testing.T) {
|
||||
t.Run("default model map must have vendor, model, and fast model", func(t *testing.T) {
|
||||
for vendor, models := range defaultModelMap {
|
||||
assert.NotEmpty(t, vendor)
|
||||
assert.NotEmpty(t, models["default"])
|
||||
assert.NotEmpty(t, models["fast"])
|
||||
}
|
||||
})
|
||||
}
|
||||
145
agent/mcp.go
Normal file
@ -0,0 +1,145 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
einomcp "github.com/cloudwego/eino-ext/components/tool/mcp"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
type McpClientToolBuilderConfig struct {
|
||||
// Common config
|
||||
ClientName string
|
||||
ClientVersion string
|
||||
|
||||
// SSE client config
|
||||
SseURL string
|
||||
Headers map[string]string
|
||||
|
||||
// Stdout client config
|
||||
SkipDefaultTools bool
|
||||
SQLQueryToolEnabled bool
|
||||
SQLQueryToolDBPath string
|
||||
PackageRegistryToolEnabled bool
|
||||
|
||||
// Enable debug mode for the MCP client.
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type mcpClientToolBuilder struct {
|
||||
config McpClientToolBuilderConfig
|
||||
}
|
||||
|
||||
var _ ToolBuilder = (*mcpClientToolBuilder)(nil)
|
||||
|
||||
// NewMcpClientToolBuilder creates a new MCP client tool builder for `vet` MCP server.
|
||||
// This basically connects to vet MCP server over SSE or executes the `vet server mcp` command
|
||||
// to start a MCP server in stdio mode. We maintain loose coupling between the MCP client and the MCP server
|
||||
// by allowing the client to be configured with a set of flags to enable/disable specific tools. We do this
|
||||
// to ensure vet MCP contract is not violated and evolves independently. vet Agents will in turn depend on
|
||||
// vet MCP server for data access.
|
||||
func NewMcpClientToolBuilder(config McpClientToolBuilderConfig) (*mcpClientToolBuilder, error) {
|
||||
return &mcpClientToolBuilder{
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *mcpClientToolBuilder) Build(ctx context.Context) ([]tool.BaseTool, error) {
|
||||
var cli *client.Client
|
||||
var err error
|
||||
|
||||
if b.config.SseURL != "" {
|
||||
cli, err = b.buildSseClient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create sse client: %w", err)
|
||||
}
|
||||
} else {
|
||||
cli, err = b.buildStdioClient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stdio client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = cli.Start(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start mcp client: %w", err)
|
||||
}
|
||||
|
||||
initRequest := mcp.InitializeRequest{}
|
||||
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
||||
initRequest.Params.ClientInfo = mcp.Implementation{
|
||||
Name: b.config.ClientName,
|
||||
Version: b.config.ClientVersion,
|
||||
}
|
||||
|
||||
_, err = cli.Initialize(ctx, initRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize mcp client: %w", err)
|
||||
}
|
||||
|
||||
tools, err := einomcp.GetTools(ctx, &einomcp.Config{
|
||||
Cli: cli,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tools: %w", err)
|
||||
}
|
||||
|
||||
return tools, nil
|
||||
}
|
||||
|
||||
func (b *mcpClientToolBuilder) buildSseClient() (*client.Client, error) {
|
||||
cli, err := client.NewSSEMCPClient(b.config.SseURL, client.WithHeaders(b.config.Headers))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create sse client: %w", err)
|
||||
}
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
// buildStdioClient is used to start vet mcp server with arguments
|
||||
// based on the configuration.
|
||||
func (b *mcpClientToolBuilder) buildStdioClient() (*client.Client, error) {
|
||||
binaryPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get running binary path: %w", err)
|
||||
}
|
||||
|
||||
// vet-mcp server defaults to stdio transport. See cmd/server/mcp.go
|
||||
vetMcpServerCommandArgs := []string{"server", "mcp"}
|
||||
|
||||
if b.config.Debug {
|
||||
vetMcpServerLogFile := filepath.Join(os.TempDir(), "vet-mcp-server.log")
|
||||
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "-l", vetMcpServerLogFile)
|
||||
}
|
||||
|
||||
if b.config.SQLQueryToolEnabled {
|
||||
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "--sql-query-tool")
|
||||
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "--sql-query-tool-db-path",
|
||||
b.config.SQLQueryToolDBPath)
|
||||
}
|
||||
|
||||
if b.config.PackageRegistryToolEnabled {
|
||||
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "--package-registry-tool")
|
||||
}
|
||||
|
||||
if b.config.SkipDefaultTools {
|
||||
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "--skip-default-tools")
|
||||
}
|
||||
|
||||
environmentVariables := []string{}
|
||||
if b.config.Debug {
|
||||
environmentVariables = append(environmentVariables, "APP_LOG_LEVEL=debug")
|
||||
}
|
||||
|
||||
cli, err := client.NewStdioMCPClient(binaryPath, environmentVariables, vetMcpServerCommandArgs...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stdio client: %w", err)
|
||||
}
|
||||
|
||||
return cli, nil
|
||||
}
|
||||
45
agent/memory.go
Normal file
@ -0,0 +1,45 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
type simpleMemory struct {
|
||||
mutex sync.RWMutex
|
||||
interactions []*schema.Message
|
||||
}
|
||||
|
||||
var _ Memory = (*simpleMemory)(nil)
|
||||
|
||||
func NewSimpleMemory() (*simpleMemory, error) {
|
||||
return &simpleMemory{
|
||||
interactions: make([]*schema.Message, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *simpleMemory) AddInteraction(ctx context.Context, interaction *schema.Message) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.interactions = append(m.interactions, interaction)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *simpleMemory) GetInteractions(ctx context.Context) ([]*schema.Message, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return m.interactions, nil
|
||||
}
|
||||
|
||||
func (m *simpleMemory) Clear(ctx context.Context) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.interactions = make([]*schema.Message, 0)
|
||||
return nil
|
||||
}
|
||||
279
agent/memory_test.go
Normal file
@ -0,0 +1,279 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewSimpleMemory(t *testing.T) {
|
||||
memory, err := NewSimpleMemory()
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, memory)
|
||||
|
||||
// Test that initial interactions are empty
|
||||
ctx := context.Background()
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(interactions))
|
||||
}
|
||||
|
||||
func TestSimpleMemory_AddInteraction(t *testing.T) {
|
||||
memory, err := NewSimpleMemory()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
message := &schema.Message{
|
||||
Role: schema.User,
|
||||
Content: "test message",
|
||||
}
|
||||
|
||||
err = memory.AddInteraction(ctx, message)
|
||||
assert.NoError(t, err)
|
||||
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(interactions))
|
||||
assert.Equal(t, message, interactions[0])
|
||||
}
|
||||
|
||||
func TestSimpleMemory_AddMultipleInteractions(t *testing.T) {
|
||||
memory, err := NewSimpleMemory()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
messages := []*schema.Message{
|
||||
{Role: schema.User, Content: "first message"},
|
||||
{Role: schema.Assistant, Content: "second message"},
|
||||
{Role: schema.User, Content: "third message"},
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
err = memory.AddInteraction(ctx, msg)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, len(interactions))
|
||||
|
||||
for i, msg := range messages {
|
||||
assert.Equal(t, msg, interactions[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleMemory_GetInteractions(t *testing.T) {
|
||||
memory, err := NewSimpleMemory()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test empty interactions
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, interactions)
|
||||
assert.Equal(t, 0, len(interactions))
|
||||
|
||||
// Add some interactions
|
||||
message1 := &schema.Message{Role: schema.User, Content: "message 1"}
|
||||
message2 := &schema.Message{Role: schema.Assistant, Content: "message 2"}
|
||||
|
||||
err = memory.AddInteraction(ctx, message1)
|
||||
require.NoError(t, err)
|
||||
err = memory.AddInteraction(ctx, message2)
|
||||
require.NoError(t, err)
|
||||
|
||||
interactions, err = memory.GetInteractions(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(interactions))
|
||||
assert.Equal(t, message1, interactions[0])
|
||||
assert.Equal(t, message2, interactions[1])
|
||||
}
|
||||
|
||||
func TestSimpleMemory_Clear(t *testing.T) {
|
||||
memory, err := NewSimpleMemory()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Add some interactions
|
||||
message1 := &schema.Message{Role: schema.User, Content: "message 1"}
|
||||
message2 := &schema.Message{Role: schema.Assistant, Content: "message 2"}
|
||||
|
||||
err = memory.AddInteraction(ctx, message1)
|
||||
require.NoError(t, err)
|
||||
err = memory.AddInteraction(ctx, message2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify interactions exist
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, len(interactions))
|
||||
|
||||
// Clear interactions
|
||||
err = memory.Clear(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify interactions are cleared
|
||||
interactions, err = memory.GetInteractions(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(interactions))
|
||||
}
|
||||
|
||||
func TestSimpleMemory_NilInteraction(t *testing.T) {
|
||||
memory, err := NewSimpleMemory()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test adding nil interaction
|
||||
err = memory.AddInteraction(ctx, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(interactions))
|
||||
assert.Nil(t, interactions[0])
|
||||
}
|
||||
|
||||
func TestSimpleMemory_ConcurrentAccess(t *testing.T) {
|
||||
memory, err := NewSimpleMemory()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
numGoroutines := 100
|
||||
messagesPerGoroutine := 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numGoroutines)
|
||||
|
||||
// Concurrent writes
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < messagesPerGoroutine; j++ {
|
||||
message := &schema.Message{
|
||||
Role: schema.User,
|
||||
Content: fmt.Sprintf("goroutine-%d-message-%d", goroutineID, j),
|
||||
}
|
||||
err := memory.AddInteraction(ctx, message)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Verify all interactions were added
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, numGoroutines*messagesPerGoroutine, len(interactions))
|
||||
}
|
||||
|
||||
func TestSimpleMemory_ConcurrentReadWrite(t *testing.T) {
|
||||
memory, err := NewSimpleMemory()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
numReaders := 10
|
||||
numWriters := 10
|
||||
messagesPerWriter := 5
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numReaders + numWriters)
|
||||
|
||||
// Concurrent writers
|
||||
for i := 0; i < numWriters; i++ {
|
||||
go func(writerID int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < messagesPerWriter; j++ {
|
||||
message := &schema.Message{
|
||||
Role: schema.User,
|
||||
Content: fmt.Sprintf("writer-%d-message-%d", writerID, j),
|
||||
}
|
||||
err := memory.AddInteraction(ctx, message)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Concurrent readers
|
||||
for i := 0; i < numReaders; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < messagesPerWriter; j++ {
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, interactions)
|
||||
// Length can vary due to concurrent writes
|
||||
assert.GreaterOrEqual(t, len(interactions), 0)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Final verification
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, numWriters*messagesPerWriter, len(interactions))
|
||||
}
|
||||
|
||||
func TestSimpleMemory_ClearDuringConcurrentAccess(t *testing.T) {
|
||||
memory, err := NewSimpleMemory()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
numWriters := 5
|
||||
messagesPerWriter := 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(numWriters + 1) // +1 for the clearer
|
||||
|
||||
// Add some initial interactions
|
||||
for i := 0; i < 5; i++ {
|
||||
message := &schema.Message{
|
||||
Role: schema.User,
|
||||
Content: fmt.Sprintf("initial-message-%d", i),
|
||||
}
|
||||
err := memory.AddInteraction(ctx, message)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Concurrent writers
|
||||
for i := 0; i < numWriters; i++ {
|
||||
go func(writerID int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < messagesPerWriter; j++ {
|
||||
message := &schema.Message{
|
||||
Role: schema.User,
|
||||
Content: fmt.Sprintf("writer-%d-message-%d", writerID, j),
|
||||
}
|
||||
err := memory.AddInteraction(ctx, message)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Clear operation
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// Clear after some writes have happened
|
||||
err := memory.Clear(ctx)
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Final state check - should be consistent
|
||||
interactions, err := memory.GetInteractions(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, interactions)
|
||||
// The exact number depends on timing of clear operation
|
||||
assert.GreaterOrEqual(t, len(interactions), 0)
|
||||
}
|
||||
156
agent/mock.go
Normal file
@ -0,0 +1,156 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
// MockAgent provides a simple implementation of the Agent interface for testing
|
||||
type mockAgent struct{}
|
||||
|
||||
// MockSession is a simple session implementation
|
||||
type mockSession struct {
|
||||
sessionID string
|
||||
memory Memory
|
||||
}
|
||||
|
||||
type mockMemory struct {
|
||||
interactions []*schema.Message
|
||||
}
|
||||
|
||||
func (m *mockMemory) AddInteraction(ctx context.Context, interaction *schema.Message) error {
|
||||
m.interactions = append(m.interactions, interaction)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockMemory) GetInteractions(ctx context.Context) ([]*schema.Message, error) {
|
||||
return m.interactions, nil
|
||||
}
|
||||
|
||||
func (m *mockMemory) Clear(ctx context.Context) error {
|
||||
m.interactions = make([]*schema.Message, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewMockAgent creates a new mock agent
|
||||
func NewMockAgent() *mockAgent {
|
||||
return &mockAgent{}
|
||||
}
|
||||
|
||||
// NewMockSession creates a new mock session
|
||||
func NewMockSession() *mockSession {
|
||||
return &mockSession{
|
||||
sessionID: "mock-session-1",
|
||||
memory: &mockMemory{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *mockSession) ID() string {
|
||||
return s.sessionID
|
||||
}
|
||||
|
||||
func (s *mockSession) Memory() Memory {
|
||||
return s.memory
|
||||
}
|
||||
|
||||
// Execute implements the Agent interface with mock responses
|
||||
func (m *mockAgent) Execute(ctx context.Context, session Session, input Input, opts ...AgentExecutionContextOpt) (Output, error) {
|
||||
// Simple mock responses based on input
|
||||
query := strings.ToLower(input.Query)
|
||||
|
||||
var response string
|
||||
|
||||
switch {
|
||||
case strings.Contains(query, "vulnerability") || strings.Contains(query, "vuln"):
|
||||
response = `🔍 **Vulnerability Analysis**
|
||||
|
||||
I found 3 critical vulnerabilities in your dependencies:
|
||||
|
||||
**Critical Issues:**
|
||||
• lodash@4.17.19: CVE-2021-23337 (Command Injection)
|
||||
• jackson-databind@2.9.8: CVE-2020-36518 (Deserialization)
|
||||
• urllib3@1.24.1: CVE-2021-33503 (SSRF)
|
||||
|
||||
**Recommendation:** Update these packages immediately. All have fixes available in newer versions.
|
||||
|
||||
Would you like me to analyze the impact of updating these packages?`
|
||||
|
||||
case strings.Contains(query, "malware") || strings.Contains(query, "malicious"):
|
||||
response = `🚨 **Malware Detection Results**
|
||||
|
||||
I detected 2 potentially malicious packages:
|
||||
|
||||
**High Risk:**
|
||||
• suspicious-package@1.0.0: Contains obfuscated code and cryptocurrency mining
|
||||
• typosquatted-lib@2.1.0: Mimics popular library with malicious payload
|
||||
|
||||
**Action Required:** Remove these packages immediately and scan your systems.
|
||||
|
||||
Would you like me to suggest secure alternatives?`
|
||||
|
||||
case strings.Contains(query, "secure") || strings.Contains(query, "security"):
|
||||
response = `🛡️ **Security Posture Assessment**
|
||||
|
||||
**Overall Security Score: 6.2/10 (Moderate Risk)**
|
||||
|
||||
**Summary:**
|
||||
• 23 total security issues found
|
||||
• 3 critical vulnerabilities requiring immediate action
|
||||
• 2 malicious packages detected
|
||||
• 15 packages with maintenance concerns
|
||||
|
||||
**Priority Actions:**
|
||||
1. Remove malicious packages (Critical)
|
||||
2. Update vulnerable dependencies (High)
|
||||
3. Implement dependency scanning in CI/CD (Medium)
|
||||
|
||||
Would you like me to create a detailed remediation plan?`
|
||||
|
||||
case strings.Contains(query, "update"):
|
||||
response = `⬆️ **Update Analysis**
|
||||
|
||||
Analyzing update recommendations for your dependencies...
|
||||
|
||||
**Safe Updates Available:**
|
||||
• 12 packages can be safely updated (patch versions)
|
||||
• 5 packages have minor version updates with new features
|
||||
• 3 packages require major version updates (breaking changes)
|
||||
|
||||
**Priority Updates:**
|
||||
1. lodash: 4.17.19 → 4.17.21 (Security fix, no breaking changes)
|
||||
2. urllib3: 1.24.1 → 1.26.18 (Security fix, minimal risk)
|
||||
|
||||
Would you like detailed impact analysis for any specific package?`
|
||||
|
||||
default:
|
||||
response = fmt.Sprintf(`🤖 **Security Analysis**
|
||||
|
||||
I'm analyzing your question about: "%s"
|
||||
|
||||
I have access to comprehensive security data including:
|
||||
• Vulnerability databases
|
||||
• Malware detection results
|
||||
• Dependency analysis
|
||||
• License compliance
|
||||
• Maintainer health metrics
|
||||
|
||||
**Available Analysis Types:**
|
||||
• Security posture assessment
|
||||
• Vulnerability impact analysis
|
||||
• Malware detection
|
||||
• Update recommendations
|
||||
• Compliance checking
|
||||
|
||||
What specific aspect would you like me to analyze in detail?`, input.Query)
|
||||
}
|
||||
|
||||
return Output{
|
||||
Answer: response,
|
||||
Format: AnswerFormatMarkdown,
|
||||
}, nil
|
||||
}
|
||||
164
agent/react.go
Normal file
@ -0,0 +1,164 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/components/tool"
|
||||
einoutils "github.com/cloudwego/eino/components/tool/utils"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
"github.com/cloudwego/eino/flow/agent/react"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
type ReactQueryAgentConfig struct {
|
||||
MaxSteps int
|
||||
SystemPrompt string
|
||||
}
|
||||
|
||||
type reactQueryAgent struct {
|
||||
config ReactQueryAgentConfig
|
||||
model model.ToolCallingChatModel
|
||||
tools []tool.BaseTool
|
||||
}
|
||||
|
||||
var _ Agent = (*reactQueryAgent)(nil)
|
||||
|
||||
type reactQueryAgentOpt func(*reactQueryAgent)
|
||||
|
||||
func WithTools(tools []tool.BaseTool) reactQueryAgentOpt {
|
||||
return func(a *reactQueryAgent) {
|
||||
a.tools = tools
|
||||
}
|
||||
}
|
||||
|
||||
func NewReactQueryAgent(model model.ToolCallingChatModel,
|
||||
config ReactQueryAgentConfig, opts ...reactQueryAgentOpt,
|
||||
) (*reactQueryAgent, error) {
|
||||
a := &reactQueryAgent{
|
||||
config: config,
|
||||
model: model,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(a)
|
||||
}
|
||||
|
||||
if a.config.MaxSteps == 0 {
|
||||
a.config.MaxSteps = 30
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *reactQueryAgent) Execute(ctx context.Context, session Session, input Input, opts ...AgentExecutionContextOpt) (Output, error) {
|
||||
executionContext := &AgentExecutionContext{}
|
||||
for _, opt := range opts {
|
||||
opt(executionContext)
|
||||
}
|
||||
|
||||
agent, err := react.NewAgent(ctx, &react.AgentConfig{
|
||||
ToolCallingModel: a.model,
|
||||
ToolsConfig: compose.ToolsNodeConfig{
|
||||
Tools: a.wrapToolsForError(a.tools),
|
||||
ToolArgumentsHandler: func(ctx context.Context, name string, arguments string) (string, error) {
|
||||
// Only allow introspection if the function is provided. Do not allow mutation.
|
||||
if executionContext.OnToolCall != nil {
|
||||
_ = executionContext.OnToolCall(ctx, session, input, name, arguments)
|
||||
}
|
||||
|
||||
return arguments, nil
|
||||
},
|
||||
},
|
||||
MaxStep: a.config.MaxSteps,
|
||||
})
|
||||
if err != nil {
|
||||
return Output{}, fmt.Errorf("failed to create react agent: %w", err)
|
||||
}
|
||||
|
||||
var messages []*schema.Message
|
||||
|
||||
// Start with the system prompt if available
|
||||
if a.config.SystemPrompt != "" {
|
||||
messages = append(messages, &schema.Message{
|
||||
Role: schema.System,
|
||||
Content: a.config.SystemPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
// Add the previous interactions to the messages
|
||||
interactions, err := session.Memory().GetInteractions(ctx)
|
||||
if err != nil {
|
||||
return Output{}, fmt.Errorf("failed to get session memory: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Add a limit to the number of interactions to avoid context bloat
|
||||
messages = append(messages, interactions...)
|
||||
|
||||
// Add the current user query message to the messages
|
||||
userQueryMsg := &schema.Message{
|
||||
Role: schema.User,
|
||||
Content: input.Query,
|
||||
}
|
||||
|
||||
messages = append(messages, userQueryMsg)
|
||||
|
||||
// Execute the agent to produce a response
|
||||
msg, err := agent.Generate(ctx, messages)
|
||||
if err != nil {
|
||||
return Output{}, fmt.Errorf("failed to generate response: %w", err)
|
||||
}
|
||||
|
||||
// Add the user query message to the session memory
|
||||
err = session.Memory().AddInteraction(ctx, userQueryMsg)
|
||||
if err != nil {
|
||||
return Output{}, fmt.Errorf("failed to add user query message to session memory: %w", err)
|
||||
}
|
||||
|
||||
// Add the agent response message to the session memory
|
||||
err = session.Memory().AddInteraction(ctx, msg)
|
||||
if err != nil {
|
||||
return Output{}, fmt.Errorf("failed to add response message to session memory: %w", err)
|
||||
}
|
||||
|
||||
return Output{
|
||||
Answer: a.schemaContent(msg),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *reactQueryAgent) wrapToolsForError(tools []tool.BaseTool) []tool.BaseTool {
|
||||
wrappedTools := make([]tool.BaseTool, len(tools))
|
||||
|
||||
for i, tool := range tools {
|
||||
wrappedTools[i] = einoutils.WrapToolWithErrorHandler(tool, func(_ context.Context, err error) string {
|
||||
errorMessage := map[string]string{
|
||||
"error": err.Error(),
|
||||
"suggestion": "Tool call failed, Please try a different approach or check your input.",
|
||||
}
|
||||
|
||||
encodedError, err := json.Marshal(errorMessage)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(encodedError)
|
||||
})
|
||||
}
|
||||
|
||||
return wrappedTools
|
||||
}
|
||||
|
||||
func (a *reactQueryAgent) schemaContent(msg *schema.Message) string {
|
||||
content := msg.Content
|
||||
|
||||
if len(msg.MultiContent) > 0 {
|
||||
content = ""
|
||||
for _, part := range msg.MultiContent {
|
||||
content += part.Text + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
29
agent/session.go
Normal file
@ -0,0 +1,29 @@
|
||||
package agent
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type session struct {
|
||||
sessionID string
|
||||
memory Memory
|
||||
}
|
||||
|
||||
var _ Session = (*session)(nil)
|
||||
|
||||
func NewSession(memory Memory) (*session, error) {
|
||||
return newSessionWithID(uuid.New().String(), memory), nil
|
||||
}
|
||||
|
||||
func newSessionWithID(id string, memory Memory) *session {
|
||||
return &session{
|
||||
sessionID: id,
|
||||
memory: memory,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *session) ID() string {
|
||||
return s.sessionID
|
||||
}
|
||||
|
||||
func (s *session) Memory() Memory {
|
||||
return s.memory
|
||||
}
|
||||
651
agent/ui.go
Normal file
@ -0,0 +1,651 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Message types for Bubbletea updates
|
||||
type statusUpdateMsg struct {
|
||||
message string
|
||||
}
|
||||
|
||||
type agentResponseMsg struct {
|
||||
content string
|
||||
}
|
||||
|
||||
type agentThinkingMsg struct {
|
||||
thinking bool
|
||||
}
|
||||
|
||||
type agentToolCallMsg struct {
|
||||
toolName string
|
||||
toolArgs string
|
||||
}
|
||||
|
||||
type thinkingTickMsg struct{}
|
||||
|
||||
var (
|
||||
headerStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240")).
|
||||
Padding(0, 1)
|
||||
|
||||
inputPromptStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240"))
|
||||
|
||||
inputCursorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("255"))
|
||||
|
||||
inputBorderStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
Padding(0, 1)
|
||||
|
||||
thinkingStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("33")).
|
||||
Bold(true)
|
||||
)
|
||||
|
||||
// AgentUI represents the main TUI model
|
||||
type agentUI struct {
|
||||
viewport viewport.Model
|
||||
textInput textarea.Model
|
||||
width int
|
||||
height int
|
||||
statusMessage string
|
||||
isThinking bool
|
||||
messages []uiMessage
|
||||
ready bool
|
||||
agent Agent
|
||||
session Session
|
||||
config AgentUIConfig
|
||||
thinkingFrame int
|
||||
inputHistory []string
|
||||
historyIndex int
|
||||
currentInput string
|
||||
}
|
||||
|
||||
// Message represents a chat message
|
||||
type uiMessage struct {
|
||||
Role string // "user", "agent", "system"
|
||||
Content string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// AgentUIConfig defines the configuration for the UI
|
||||
type AgentUIConfig struct {
|
||||
Width int
|
||||
Height int
|
||||
InitialSystemMessage string
|
||||
TextInputPlaceholder string
|
||||
TitleText string
|
||||
MaxHistory int
|
||||
|
||||
// Only for informational purposes.
|
||||
ModelName string
|
||||
ModelVendor string
|
||||
ModelFast bool
|
||||
}
|
||||
|
||||
// DefaultAgentUIConfig returns the opinionated default configuration for the UI
|
||||
func DefaultAgentUIConfig() AgentUIConfig {
|
||||
return AgentUIConfig{
|
||||
Width: 80,
|
||||
Height: 20,
|
||||
MaxHistory: 50,
|
||||
InitialSystemMessage: "Security Agent initialized",
|
||||
TextInputPlaceholder: "Ask me anything...",
|
||||
TitleText: "Security Agent",
|
||||
}
|
||||
}
|
||||
|
||||
// NewAgentUI creates a new agent UI instance
|
||||
func NewAgentUI(agent Agent, session Session, config AgentUIConfig) *agentUI {
|
||||
vp := viewport.New(config.Width, config.Height)
|
||||
|
||||
ta := textarea.New()
|
||||
ta.Placeholder = ""
|
||||
ta.Focus()
|
||||
ta.SetHeight(1)
|
||||
ta.SetWidth(80)
|
||||
ta.CharLimit = 1000
|
||||
ta.ShowLineNumbers = false
|
||||
|
||||
ui := &agentUI{
|
||||
viewport: vp,
|
||||
textInput: ta,
|
||||
statusMessage: "",
|
||||
messages: []uiMessage{},
|
||||
agent: agent,
|
||||
session: session,
|
||||
config: config,
|
||||
thinkingFrame: 0,
|
||||
inputHistory: []string{},
|
||||
historyIndex: -1,
|
||||
currentInput: "",
|
||||
}
|
||||
|
||||
ui.addSystemMessage(config.InitialSystemMessage)
|
||||
|
||||
return ui
|
||||
}
|
||||
|
||||
// Init implements the tea.Model interface
|
||||
func (m *agentUI) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
textarea.Blink,
|
||||
m.tickThinking(),
|
||||
)
|
||||
}
|
||||
|
||||
// Update implements the tea.Model interface
|
||||
func (m *agentUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
return m, tea.Quit
|
||||
|
||||
case tea.KeyEnter:
|
||||
if m.textInput.Focused() && !m.isThinking {
|
||||
// Handle user input only if agent is not in thinking mode
|
||||
userInput := strings.TrimSpace(m.textInput.Value())
|
||||
if userInput != "" {
|
||||
// Add to history and reset navigation
|
||||
m.addToHistory(userInput)
|
||||
|
||||
// Add the input to the message list and reset user input field
|
||||
m.addUserMessage(userInput)
|
||||
m.resetInputField()
|
||||
|
||||
// Check if it's a slash command
|
||||
if strings.HasPrefix(userInput, "/") {
|
||||
// Handle slash command
|
||||
cmd := m.handleSlashCommand(userInput)
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
} else {
|
||||
// Execute agent query
|
||||
cmds = append(cmds,
|
||||
m.setThinking(true),
|
||||
m.executeAgentQuery(userInput),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case tea.KeyTab:
|
||||
// Switch focus between input and viewport, but not while agent is thinking
|
||||
if !m.isThinking {
|
||||
if m.textInput.Focused() {
|
||||
m.textInput.Blur()
|
||||
} else {
|
||||
m.textInput.Focus()
|
||||
cmds = append(cmds, textarea.Blink)
|
||||
}
|
||||
}
|
||||
|
||||
case tea.KeyUp, tea.KeyDown:
|
||||
if m.textInput.Focused() && !m.isThinking {
|
||||
// Navigate input history when text input is focused
|
||||
var direction int
|
||||
if msg.Type == tea.KeyUp {
|
||||
direction = 1 // Go back in history
|
||||
} else {
|
||||
direction = -1 // Go forward in history
|
||||
}
|
||||
|
||||
historyEntry := m.navigateHistory(direction)
|
||||
m.textInput.SetValue(historyEntry)
|
||||
m.textInput.CursorEnd()
|
||||
} else if !m.textInput.Focused() {
|
||||
// Allow scrolling in viewport when not focused on text input
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
case tea.KeyPgUp, tea.KeyPgDown:
|
||||
// Allow scrolling in viewport when not focused on text input
|
||||
if !m.textInput.Focused() {
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
case tea.KeyHome:
|
||||
if !m.textInput.Focused() {
|
||||
m.viewport.GotoTop()
|
||||
}
|
||||
|
||||
case tea.KeyEnd:
|
||||
if !m.textInput.Focused() {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
// Handle window resize
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
|
||||
// Calculate dimensions for minimal UI
|
||||
headerHeight := 2 // Header + blank line
|
||||
inputHeight := 2 // Input area + status
|
||||
spacing := 1 // Bottom spacing
|
||||
|
||||
// Calculate viewport dimensions to maximize output area
|
||||
viewportHeight := m.height - headerHeight - inputHeight - spacing
|
||||
|
||||
// Ensure minimum height
|
||||
if viewportHeight < 10 {
|
||||
viewportHeight = 10
|
||||
}
|
||||
|
||||
// Full width utilization
|
||||
viewportWidth := m.width
|
||||
|
||||
// Ensure minimum width
|
||||
if viewportWidth < 50 {
|
||||
viewportWidth = 50
|
||||
}
|
||||
|
||||
m.viewport.Width = viewportWidth
|
||||
m.viewport.Height = viewportHeight
|
||||
m.textInput.SetWidth(m.width - 3)
|
||||
|
||||
// Update content when dimensions change
|
||||
m.viewport.SetContent(m.renderMessages())
|
||||
|
||||
if !m.ready {
|
||||
m.ready = true
|
||||
}
|
||||
|
||||
case statusUpdateMsg:
|
||||
m.statusMessage = msg.message
|
||||
|
||||
case agentThinkingMsg:
|
||||
m.isThinking = msg.thinking
|
||||
|
||||
// When agent starts thinking, blur the input
|
||||
if m.isThinking {
|
||||
m.resetInputField()
|
||||
m.textInput.Blur()
|
||||
m.thinkingFrame = 0
|
||||
cmds = append(cmds, m.tickThinking())
|
||||
} else {
|
||||
// Re-focus input when thinking stops
|
||||
m.textInput.Focus()
|
||||
cmds = append(cmds, textarea.Blink)
|
||||
}
|
||||
|
||||
case agentResponseMsg:
|
||||
m.addAgentMessage(msg.content)
|
||||
cmds = append(cmds, m.setThinking(false))
|
||||
|
||||
case agentToolCallMsg:
|
||||
m.addToolCallMessage(fmt.Sprintf("🔧 %s", msg.toolName), msg.toolArgs)
|
||||
|
||||
case thinkingTickMsg:
|
||||
if m.isThinking {
|
||||
m.thinkingFrame = (m.thinkingFrame + 1) % 4
|
||||
cmds = append(cmds, m.tickThinking())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Update child components
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
// Only update text input if not thinking
|
||||
if !m.isThinking {
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// View implements the tea.Model interface
|
||||
func (m *agentUI) View() string {
|
||||
if !m.ready {
|
||||
return "Loading..."
|
||||
}
|
||||
|
||||
if m.width == 0 || m.height == 0 {
|
||||
return "Initializing..."
|
||||
}
|
||||
|
||||
modelAbility := "fast"
|
||||
if !m.config.ModelFast {
|
||||
modelAbility = "slow"
|
||||
}
|
||||
|
||||
modelStatusLine := fmt.Sprintf("%s/%s (%s)", m.config.ModelVendor, m.config.ModelName, modelAbility)
|
||||
|
||||
header := headerStyle.Render(fmt.Sprintf("%s %s", m.config.TitleText, modelStatusLine))
|
||||
|
||||
content := m.viewport.View()
|
||||
|
||||
var thinkingIndicator string
|
||||
if m.isThinking {
|
||||
thinkingFrames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||
spinner := thinkingFrames[m.thinkingFrame%len(thinkingFrames)]
|
||||
thinkingIndicator = thinkingStyle.Render(fmt.Sprintf("%s thinking...", spinner))
|
||||
}
|
||||
|
||||
var inputArea string
|
||||
userInput := m.textInput.Value()
|
||||
cursor := ""
|
||||
if m.textInput.Focused() && !m.isThinking {
|
||||
cursor = inputCursorStyle.Render("▊")
|
||||
}
|
||||
|
||||
inputContent := fmt.Sprintf("%s%s%s", inputPromptStyle.Render("> "), userInput, cursor)
|
||||
inputArea = inputBorderStyle.Width(m.width - 2).Render(inputContent)
|
||||
|
||||
statusLine := inputPromptStyle.Render(fmt.Sprintf("** %s | ctrl+c to exit", modelStatusLine))
|
||||
|
||||
var components []string
|
||||
components = append(components, header, "", content, "")
|
||||
|
||||
if thinkingIndicator != "" {
|
||||
components = append(components, thinkingIndicator)
|
||||
}
|
||||
|
||||
components = append(components, inputArea, statusLine)
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, components...)
|
||||
}
|
||||
|
||||
func (m *agentUI) resetInputField() {
|
||||
m.textInput.Reset()
|
||||
m.textInput.SetValue("")
|
||||
m.textInput.CursorStart()
|
||||
}
|
||||
|
||||
func (m *agentUI) addUserMessage(content string) {
|
||||
m.messages = append(m.messages, uiMessage{
|
||||
Role: "user",
|
||||
Content: content,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
m.viewport.SetContent(m.renderMessages())
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
|
||||
func (m *agentUI) addAgentMessage(content string) {
|
||||
m.messages = append(m.messages, uiMessage{
|
||||
Role: "agent",
|
||||
Content: content,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
m.viewport.SetContent(m.renderMessages())
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
|
||||
func (m *agentUI) addSystemMessage(content string) {
|
||||
m.messages = append(m.messages, uiMessage{
|
||||
Role: "system",
|
||||
Content: content,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
m.viewport.SetContent(m.renderMessages())
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
|
||||
func (m *agentUI) addToolCallMessage(toolName string, toolArgs string) {
|
||||
content := fmt.Sprintf(" %s", toolName)
|
||||
if toolArgs != "" && toolArgs != "{}" {
|
||||
content += fmt.Sprintf("\n └─ %s", toolArgs)
|
||||
}
|
||||
|
||||
m.messages = append(m.messages, uiMessage{
|
||||
Role: "tool",
|
||||
Content: content,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
m.viewport.SetContent(m.renderMessages())
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
|
||||
// renderMessages formats all messages for display
|
||||
func (m *agentUI) renderMessages() string {
|
||||
var rendered []string
|
||||
|
||||
rendered = append(rendered, "", "")
|
||||
|
||||
contentWidth := m.viewport.Width - 2 // Account for internal padding
|
||||
if contentWidth < 40 {
|
||||
contentWidth = 40
|
||||
}
|
||||
|
||||
r, err := glamour.NewTermRenderer(
|
||||
glamour.WithStandardStyle("notty"),
|
||||
glamour.WithWordWrap(contentWidth),
|
||||
)
|
||||
if err != nil {
|
||||
r = nil
|
||||
}
|
||||
|
||||
for _, msg := range m.messages {
|
||||
timestamp := msg.Timestamp.Format("15:04:05")
|
||||
|
||||
switch msg.Role {
|
||||
case "user":
|
||||
userHeaderStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("86")).
|
||||
Bold(true).
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(lipgloss.Color("86")).
|
||||
Padding(0, 1)
|
||||
|
||||
userContentStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("255")).
|
||||
Padding(0, 2)
|
||||
|
||||
rendered = append(rendered,
|
||||
userHeaderStyle.Render(fmt.Sprintf("[%s] → You:", timestamp)),
|
||||
userContentStyle.Render(msg.Content),
|
||||
"",
|
||||
)
|
||||
case "agent":
|
||||
var content string
|
||||
if r != nil {
|
||||
renderedMarkdown, err := r.Render(msg.Content)
|
||||
if err == nil {
|
||||
content = strings.TrimSpace(renderedMarkdown)
|
||||
} else {
|
||||
content = msg.Content // Fallback to plain text
|
||||
}
|
||||
} else {
|
||||
content = msg.Content
|
||||
}
|
||||
|
||||
agentHeaderStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("39")).
|
||||
Bold(true).
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(lipgloss.Color("39")).
|
||||
Padding(0, 1)
|
||||
|
||||
agentContentStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("255")).
|
||||
Padding(0, 2)
|
||||
|
||||
rendered = append(rendered,
|
||||
agentHeaderStyle.Render(fmt.Sprintf("[%s] ← Agent:", timestamp)),
|
||||
agentContentStyle.Render(content),
|
||||
"",
|
||||
)
|
||||
case "system":
|
||||
systemStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("241")).
|
||||
Italic(true).
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(lipgloss.Color("241")).
|
||||
Padding(0, 1)
|
||||
|
||||
rendered = append(rendered,
|
||||
systemStyle.Render(fmt.Sprintf("[%s] ℹ %s", timestamp, msg.Content)),
|
||||
"",
|
||||
)
|
||||
case "tool":
|
||||
toolStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("245")).
|
||||
Italic(true).
|
||||
Faint(true).
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(lipgloss.Color("245")).
|
||||
Padding(0, 1)
|
||||
|
||||
rendered = append(rendered,
|
||||
toolStyle.Render(fmt.Sprintf("[%s] %s", timestamp, msg.Content)),
|
||||
"",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
rendered = append(rendered, "", "")
|
||||
|
||||
return strings.Join(rendered, "\n")
|
||||
}
|
||||
|
||||
func (m *agentUI) updateStatus(message string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return statusUpdateMsg{message: message}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *agentUI) setThinking(thinking bool) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return agentThinkingMsg{thinking: thinking}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *agentUI) executeAgentQuery(userInput string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
input := Input{
|
||||
Query: userInput,
|
||||
}
|
||||
|
||||
toolCallHook := func(_ context.Context, _ Session, _ Input, toolName string, toolArgs string) error {
|
||||
m.Update(agentToolCallMsg{toolName: toolName, toolArgs: toolArgs})
|
||||
return nil
|
||||
}
|
||||
|
||||
output, err := m.agent.Execute(ctx, m.session, input, WithToolCallHook(toolCallHook))
|
||||
if err != nil {
|
||||
return agentResponseMsg{
|
||||
content: fmt.Sprintf("❌ **Error**\n\nSorry, I encountered an error while processing your query:\n\n%s", err.Error()),
|
||||
}
|
||||
}
|
||||
|
||||
return agentResponseMsg{content: output.Answer}
|
||||
}
|
||||
}
|
||||
|
||||
// StartUI starts the TUI application with the default configuration
|
||||
func StartUI(agent Agent, session Session) error {
|
||||
config := DefaultAgentUIConfig()
|
||||
config.InitialSystemMessage = ""
|
||||
return StartUIWithConfig(agent, session, config)
|
||||
}
|
||||
|
||||
// StartUIWithConfig starts the TUI application with the provided configuration
|
||||
func StartUIWithConfig(agent Agent, session Session, config AgentUIConfig) error {
|
||||
ui := NewAgentUI(agent, session, config)
|
||||
|
||||
p := tea.NewProgram(
|
||||
ui,
|
||||
tea.WithAltScreen(),
|
||||
tea.WithMouseCellMotion(),
|
||||
)
|
||||
|
||||
_, err := p.Run()
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *agentUI) tickThinking() tea.Cmd {
|
||||
return tea.Tick(150*time.Millisecond, func(time.Time) tea.Msg {
|
||||
return thinkingTickMsg{}
|
||||
})
|
||||
}
|
||||
|
||||
// handleSlashCommand processes commands that start with '/'
|
||||
func (m *agentUI) handleSlashCommand(command string) tea.Cmd {
|
||||
switch command {
|
||||
case "/exit":
|
||||
m.addSystemMessage("Goodbye! Exiting gracefully...")
|
||||
return tea.Quit
|
||||
default:
|
||||
m.addSystemMessage(fmt.Sprintf("Unknown command: %s", command))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// addToHistory adds input to history buffer with a maximum of 50 entries
|
||||
func (m *agentUI) addToHistory(input string) {
|
||||
// Don't add empty strings or duplicates of the last entry
|
||||
if input == "" || (len(m.inputHistory) > 0 && m.inputHistory[len(m.inputHistory)-1] == input) {
|
||||
return
|
||||
}
|
||||
|
||||
m.inputHistory = append(m.inputHistory, input)
|
||||
|
||||
// Keep only the last maxHistory entries
|
||||
if len(m.inputHistory) > m.config.MaxHistory {
|
||||
m.inputHistory = m.inputHistory[len(m.inputHistory)-m.config.MaxHistory:]
|
||||
}
|
||||
|
||||
// Reset history navigation
|
||||
m.historyIndex = -1
|
||||
m.currentInput = ""
|
||||
}
|
||||
|
||||
// navigateHistory moves through input history and returns the selected entry
|
||||
func (m *agentUI) navigateHistory(direction int) string {
|
||||
if len(m.inputHistory) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Save current input when starting navigation
|
||||
if m.historyIndex == -1 {
|
||||
m.currentInput = m.textInput.Value()
|
||||
}
|
||||
|
||||
// Calculate new index
|
||||
newIndex := m.historyIndex + direction
|
||||
|
||||
// Handle boundaries
|
||||
if newIndex < -1 {
|
||||
newIndex = -1
|
||||
} else if newIndex >= len(m.inputHistory) {
|
||||
newIndex = len(m.inputHistory) - 1
|
||||
}
|
||||
|
||||
m.historyIndex = newIndex
|
||||
|
||||
// Return the appropriate entry
|
||||
if m.historyIndex == -1 {
|
||||
return m.currentInput
|
||||
}
|
||||
|
||||
return m.inputHistory[len(m.inputHistory)-1-m.historyIndex]
|
||||
}
|
||||
280
agent/ui_test.go
Normal file
@ -0,0 +1,280 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAgentUICreation(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
|
||||
assert.NotNil(t, ui, "Failed to create AgentUI")
|
||||
assert.Empty(t, ui.statusMessage, "Expected empty status message")
|
||||
assert.False(t, ui.isThinking, "UI should not be thinking initially")
|
||||
assert.Equal(t, 0, ui.thinkingFrame, "Thinking frame should be 0 initially")
|
||||
|
||||
// Check that system message was added if config has one
|
||||
if config.InitialSystemMessage != "" {
|
||||
assert.NotEmpty(t, ui.messages, "Expected system message to be added if InitialSystemMessage is set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAgentUIConfig(t *testing.T) {
|
||||
config := DefaultAgentUIConfig()
|
||||
|
||||
assert.Equal(t, 80, config.Width, "Expected default width 80")
|
||||
assert.Equal(t, 20, config.Height, "Expected default height 20")
|
||||
assert.Equal(t, "Security Agent", config.TitleText, "Expected title 'Security Agent'")
|
||||
assert.Equal(t, "Ask me anything...", config.TextInputPlaceholder, "Expected placeholder 'Ask me anything...'")
|
||||
}
|
||||
|
||||
func TestMessageManagement(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
|
||||
initialCount := len(ui.messages)
|
||||
|
||||
// Test adding user message
|
||||
ui.addUserMessage("Test user message")
|
||||
assert.Equal(t, initialCount+1, len(ui.messages), "Expected message count to increase")
|
||||
|
||||
lastMessage := ui.messages[len(ui.messages)-1]
|
||||
assert.Equal(t, "user", lastMessage.Role, "Expected last message role to be 'user'")
|
||||
assert.Equal(t, "Test user message", lastMessage.Content, "Expected last message content to be 'Test user message'")
|
||||
|
||||
// Test adding agent message
|
||||
ui.addAgentMessage("Test agent response")
|
||||
assert.Equal(t, initialCount+2, len(ui.messages), "Expected message count to increase")
|
||||
|
||||
lastMessage = ui.messages[len(ui.messages)-1]
|
||||
assert.Equal(t, "agent", lastMessage.Role, "Expected last message role to be 'agent'")
|
||||
|
||||
// Test adding system message
|
||||
ui.addSystemMessage("System notification")
|
||||
assert.Equal(t, initialCount+3, len(ui.messages), "Expected message count to increase")
|
||||
|
||||
lastMessage = ui.messages[len(ui.messages)-1]
|
||||
assert.Equal(t, "system", lastMessage.Role, "Expected last message role to be 'system'")
|
||||
|
||||
// Test adding tool call message
|
||||
ui.addToolCallMessage("ScanVulnerabilities", `{"path": "/app"}`)
|
||||
assert.Equal(t, initialCount+4, len(ui.messages), "Expected message count to increase")
|
||||
|
||||
lastMessage = ui.messages[len(ui.messages)-1]
|
||||
assert.Equal(t, "tool", lastMessage.Role, "Expected last message role to be 'tool'")
|
||||
}
|
||||
|
||||
func TestMessageRendering(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
|
||||
// Set up viewport dimensions for rendering
|
||||
ui.viewport.Width = 80
|
||||
ui.viewport.Height = 20
|
||||
|
||||
ui.addUserMessage("How many vulnerabilities?")
|
||||
ui.addAgentMessage("Found 5 critical vulnerabilities")
|
||||
|
||||
rendered := ui.renderMessages()
|
||||
|
||||
assert.NotEmpty(t, rendered, "Expected non-empty rendered output")
|
||||
assert.Contains(t, rendered, "How many vulnerabilities?", "Rendered output should contain user message")
|
||||
assert.Contains(t, rendered, "Found 5 critical vulnerabilities", "Rendered output should contain agent message")
|
||||
assert.Contains(t, rendered, "You:", "Rendered output should contain user label")
|
||||
assert.Contains(t, rendered, "Agent:", "Rendered output should contain agent label")
|
||||
}
|
||||
|
||||
func TestViewportDimensions(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
|
||||
// Test window resize handling
|
||||
resizeMsg := tea.WindowSizeMsg{Width: 100, Height: 30}
|
||||
ui.Update(resizeMsg)
|
||||
|
||||
assert.Equal(t, 100, ui.width, "Expected width 100")
|
||||
assert.Equal(t, 30, ui.height, "Expected height 30")
|
||||
|
||||
// Test minimum dimensions enforcement
|
||||
resizeMsg = tea.WindowSizeMsg{Width: 10, Height: 5}
|
||||
ui.Update(resizeMsg)
|
||||
|
||||
assert.GreaterOrEqual(t, ui.viewport.Width, 50, "Viewport width should be enforced to minimum 50")
|
||||
assert.GreaterOrEqual(t, ui.viewport.Height, 10, "Viewport height should be enforced to minimum 10")
|
||||
}
|
||||
|
||||
func TestViewRendering(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
config.ModelName = "gpt-4"
|
||||
config.ModelVendor = "openai"
|
||||
config.ModelFast = false
|
||||
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
ui.width = 80
|
||||
ui.height = 24
|
||||
ui.ready = true
|
||||
|
||||
view := ui.View()
|
||||
|
||||
assert.Contains(t, view, "Security Agent", "View should contain title")
|
||||
assert.Contains(t, view, "openai/gpt-4", "View should contain model information")
|
||||
assert.Contains(t, view, ">", "View should contain input prompt")
|
||||
assert.Contains(t, view, "ctrl+c to exit", "View should contain exit instruction")
|
||||
}
|
||||
|
||||
func TestThinkingState(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
ui.width = 80
|
||||
ui.height = 24
|
||||
ui.ready = true
|
||||
|
||||
// Initially not thinking
|
||||
assert.False(t, ui.isThinking, "UI should not be thinking initially")
|
||||
|
||||
// Set thinking state
|
||||
thinkingMsg := agentThinkingMsg{thinking: true}
|
||||
ui.Update(thinkingMsg)
|
||||
|
||||
assert.True(t, ui.isThinking, "UI should be thinking after agentThinkingMsg")
|
||||
|
||||
// Check view contains thinking indicator
|
||||
view := ui.View()
|
||||
assert.Contains(t, view, "thinking...", "View should contain thinking indicator when thinking")
|
||||
|
||||
// Stop thinking
|
||||
thinkingMsg = agentThinkingMsg{thinking: false}
|
||||
ui.Update(thinkingMsg)
|
||||
|
||||
assert.False(t, ui.isThinking, "UI should not be thinking after agentThinkingMsg with false")
|
||||
}
|
||||
|
||||
func TestKeyboardHandling(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
ui.width = 80
|
||||
ui.height = 24
|
||||
ui.ready = true
|
||||
|
||||
var keyMsg tea.KeyMsg
|
||||
var model tea.Model
|
||||
var cmd tea.Cmd
|
||||
|
||||
// Test Ctrl+C exits immediately
|
||||
keyMsg = tea.KeyMsg{Type: tea.KeyCtrlC}
|
||||
_, cmd = ui.Update(keyMsg)
|
||||
|
||||
assert.NotNil(t, cmd, "Ctrl+C should return quit command")
|
||||
|
||||
// Test Tab key for focus switching when not thinking
|
||||
ui.textInput.Focus()
|
||||
keyMsg = tea.KeyMsg{Type: tea.KeyTab}
|
||||
model, _ = ui.Update(keyMsg)
|
||||
ui = model.(*agentUI)
|
||||
|
||||
assert.False(t, ui.textInput.Focused(), "Tab should blur text input when it's focused")
|
||||
|
||||
// Test Enter key handling when not thinking
|
||||
ui.textInput.Focus()
|
||||
ui.textInput.SetValue("test message")
|
||||
initialMessageCount := len(ui.messages)
|
||||
|
||||
keyMsg = tea.KeyMsg{Type: tea.KeyEnter}
|
||||
model, _ = ui.Update(keyMsg)
|
||||
ui = model.(*agentUI)
|
||||
|
||||
assert.Equal(t, initialMessageCount+1, len(ui.messages), "Enter should add user message when input is not empty")
|
||||
|
||||
// Note: Input field reset happens when thinking starts, not immediately
|
||||
// The resetInputField() is called, but the UI state may not reflect it immediately in tests
|
||||
}
|
||||
|
||||
func TestInputFieldReset(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
|
||||
// Set some input text
|
||||
ui.textInput.SetValue("test input")
|
||||
assert.Equal(t, "test input", ui.textInput.Value(), "Input should contain test text")
|
||||
|
||||
// Reset input field
|
||||
ui.resetInputField()
|
||||
assert.Empty(t, ui.textInput.Value(), "Input should be empty after reset")
|
||||
}
|
||||
|
||||
func TestCommandCreation(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
|
||||
// Test status update command
|
||||
cmd := ui.updateStatus("Testing status")
|
||||
assert.NotNil(t, cmd, "updateStatus should return a non-nil command")
|
||||
|
||||
// Test thinking command
|
||||
cmd = ui.setThinking(true)
|
||||
assert.NotNil(t, cmd, "setThinking should return a non-nil command")
|
||||
|
||||
// Test execute agent query command
|
||||
cmd = ui.executeAgentQuery("test query")
|
||||
assert.NotNil(t, cmd, "executeAgentQuery should return a non-nil command")
|
||||
}
|
||||
|
||||
func TestMessageTimestamps(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
|
||||
before := time.Now()
|
||||
ui.addUserMessage("Test message")
|
||||
after := time.Now()
|
||||
|
||||
message := ui.messages[len(ui.messages)-1]
|
||||
|
||||
assert.True(t, message.Timestamp.After(before) || message.Timestamp.Equal(before), "Message timestamp should be after or equal to before time")
|
||||
assert.True(t, message.Timestamp.Before(after) || message.Timestamp.Equal(after), "Message timestamp should be before or equal to after time")
|
||||
}
|
||||
|
||||
func TestUIInitialization(t *testing.T) {
|
||||
mockAgent := NewMockAgent()
|
||||
mockSession := NewMockSession()
|
||||
config := DefaultAgentUIConfig()
|
||||
ui := NewAgentUI(mockAgent, mockSession, config)
|
||||
|
||||
// Test Init command
|
||||
cmd := ui.Init()
|
||||
assert.NotNil(t, cmd, "Init should return a non-nil command")
|
||||
|
||||
// Test initial state before ready
|
||||
view := ui.View()
|
||||
assert.Equal(t, "Loading...", view, "View should show loading before ready")
|
||||
|
||||
// Test with zero dimensions
|
||||
ui.ready = true
|
||||
ui.width = 0
|
||||
ui.height = 0
|
||||
view = ui.View()
|
||||
assert.Equal(t, "Initializing...", view, "View should show initializing with zero dimensions")
|
||||
}
|
||||
53
cmd/agent/common.go
Normal file
@ -0,0 +1,53 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
|
||||
"github.com/safedep/vet/agent"
|
||||
)
|
||||
|
||||
func buildModelFromEnvironment() (*agent.Model, error) {
|
||||
model, err := agent.BuildModelFromEnvironment(fastMode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build LLM model adapter using environment configuration: %w", err)
|
||||
}
|
||||
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func executeAgentPrompt(agentExecutor agent.Agent, session agent.Session, prompt string) error {
|
||||
output, err := agentExecutor.Execute(context.Background(), session, agent.Input{
|
||||
Query: prompt,
|
||||
}, agent.WithToolCallHook(func(ctx context.Context, session agent.Session, input agent.Input, toolName string, toolArgs string) error {
|
||||
os.Stderr.WriteString(fmt.Sprintf("Tool called: %s with args: %s\n", toolName, toolArgs))
|
||||
return nil
|
||||
}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute agent: %w", err)
|
||||
}
|
||||
|
||||
terminalRenderer, err := glamour.NewTermRenderer(
|
||||
glamour.WithAutoStyle(),
|
||||
glamour.WithWordWrap(80),
|
||||
glamour.WithEmoji(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create glamour renderer: %w", err)
|
||||
}
|
||||
|
||||
rendered, err := terminalRenderer.Render(output.Answer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render answer: %w", err)
|
||||
}
|
||||
|
||||
_, err = os.Stdout.WriteString(rendered)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write answer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
34
cmd/agent/main.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Package agent provides a CLI for running agents.
|
||||
package agent
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var (
|
||||
maxAgentSteps int
|
||||
|
||||
// Use a fast model when available. Opinionated. Can be overridden by the
|
||||
// setting environment variables.
|
||||
fastMode bool
|
||||
|
||||
// User wants the agent to answer a single question and not start the
|
||||
// interactive agent. Not all agents may support this.
|
||||
singlePrompt string
|
||||
)
|
||||
|
||||
func NewAgentCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "agent",
|
||||
Short: "Run an available AI agent",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().IntVar(&maxAgentSteps, "max-steps", 30, "The maximum number of steps for the agent executor")
|
||||
cmd.PersistentFlags().StringVarP(&singlePrompt, "prompt", "p", "", "A single prompt to run the agent with")
|
||||
cmd.PersistentFlags().BoolVar(&fastMode, "fast", false, "Prefer a fast model when available (compromises on advanced reasoning)")
|
||||
|
||||
cmd.AddCommand(newQueryAgentCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
107
cmd/agent/query.go
Normal file
@ -0,0 +1,107 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/agent"
|
||||
"github.com/safedep/vet/internal/analytics"
|
||||
"github.com/safedep/vet/internal/command"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
)
|
||||
|
||||
//go:embed query_prompt.md
|
||||
var querySystemPrompt string
|
||||
|
||||
var queryAgentDBPath string
|
||||
|
||||
func newQueryAgentCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "query",
|
||||
Short: "Query agent allows analysis and querying the vet sqlite3 report database",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := executeQueryAgent()
|
||||
if err != nil {
|
||||
logger.Errorf("failed to execute query agent: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&queryAgentDBPath, "db", "", "The path to the vet sqlite3 report database")
|
||||
|
||||
_ = cmd.MarkFlagRequired("db")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func executeQueryAgent() error {
|
||||
analytics.TrackAgentQuery()
|
||||
|
||||
toolBuilder, err := agent.NewMcpClientToolBuilder(agent.McpClientToolBuilderConfig{
|
||||
ClientName: "vet-query-agent",
|
||||
ClientVersion: command.GetVersion(),
|
||||
SkipDefaultTools: true,
|
||||
SQLQueryToolEnabled: true,
|
||||
SQLQueryToolDBPath: queryAgentDBPath,
|
||||
PackageRegistryToolEnabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create MCP client tool builder: %w", err)
|
||||
}
|
||||
|
||||
tools, err := toolBuilder.Build(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build tools: %w", err)
|
||||
}
|
||||
|
||||
model, err := buildModelFromEnvironment()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build LLM model adapter using environment configuration: %w", err)
|
||||
}
|
||||
|
||||
agentExecutor, err := agent.NewReactQueryAgent(model.Client, agent.ReactQueryAgentConfig{
|
||||
MaxSteps: maxAgentSteps,
|
||||
SystemPrompt: querySystemPrompt,
|
||||
}, agent.WithTools(tools))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create agent: %w", err)
|
||||
}
|
||||
|
||||
memory, err := agent.NewSimpleMemory()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create memory: %w", err)
|
||||
}
|
||||
|
||||
session, err := agent.NewSession(memory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
|
||||
if singlePrompt != "" {
|
||||
err = executeAgentPrompt(agentExecutor, session, singlePrompt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute agent prompt: %w", err)
|
||||
}
|
||||
} else {
|
||||
|
||||
uiConfig := agent.DefaultAgentUIConfig()
|
||||
uiConfig.TitleText = "🔍 Query Agent - Interactive Query Mode"
|
||||
uiConfig.TextInputPlaceholder = "Ask me anything about your scan data..."
|
||||
uiConfig.InitialSystemMessage = "🤖 Query Agent initialized. Ask me anything about your dependencies, vulnerabilities and other supply chain risks."
|
||||
uiConfig.ModelName = model.Name
|
||||
uiConfig.ModelVendor = model.Vendor
|
||||
uiConfig.ModelFast = model.Fast
|
||||
|
||||
err = agent.StartUIWithConfig(agentExecutor, session, uiConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start agent interaction UI: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
57
cmd/agent/query_prompt.md
Normal file
@ -0,0 +1,57 @@
|
||||
Your task is to assist the user in finding useful information from vet scan results
|
||||
available in an sqlite3 database.
|
||||
|
||||
To answer user's query, you MUST do the following:
|
||||
|
||||
1. **Schema Discovery**: Use the database schema introspection tool to understand the available tables, columns, and relationships
|
||||
2. **Query Planning**: Analyze the user's question and plan your approach:
|
||||
- Identify which tables contain the relevant data
|
||||
- Determine the relationships between tables needed
|
||||
- Plan the query structure before writing SQL
|
||||
3. **Query Execution**: Execute your planned query using the database query tool
|
||||
4. **Result Validation**: Verify the results make sense and answer the user's question
|
||||
5. **Response Formatting**: Present findings in clear markdown format
|
||||
|
||||
GUIDELINES:
|
||||
|
||||
* **Query Best Practices**:
|
||||
- Always use `COUNT(*)` instead of `SELECT *` when determining table sizes
|
||||
- Use `LIMIT` and `OFFSET` for pagination with large result sets
|
||||
- Prefer JOINs over subqueries for better performance
|
||||
- Use aggregate functions (COUNT, SUM, AVG) for statistical queries
|
||||
|
||||
* **Data Integrity**:
|
||||
- NEVER make assumptions about data that you haven't verified through queries
|
||||
- If a query returns unexpected results, re-examine your approach
|
||||
- Always check for NULL values and handle them appropriately
|
||||
- Validate that your query logic matches the user's intent
|
||||
|
||||
* **Error Handling**:
|
||||
- If a query fails, explain the error and try an alternative approach
|
||||
- If no data is found, clearly state this rather than making assumptions
|
||||
- When data seems incomplete, acknowledge limitations in your response
|
||||
|
||||
IMPORTANT CONSTRAINTS:
|
||||
|
||||
* **Prevent Hallucinations**:
|
||||
- Only report data that you have actually queried from the database
|
||||
- NEVER invent or assume data points that weren't returned by your queries
|
||||
- If you're unsure about a result, query the data again to confirm
|
||||
- Always distinguish between actual data and your interpretation of it
|
||||
|
||||
* **User Interaction**:
|
||||
- Ask for clarification if the user's query is ambiguous
|
||||
- Provide context about what the data represents (e.g., "This shows vulnerabilities found in your dependencies")
|
||||
- If you cannot answer with available data, explain what information is missing
|
||||
|
||||
* **Response Format**:
|
||||
- Present tabular data as markdown tables with appropriate headers
|
||||
- Include summary statistics when relevant (e.g., "Found 15 vulnerabilities across 8 packages")
|
||||
- Use clear headings to organize complex responses
|
||||
- Always explain what the data means in the context of security scanning
|
||||
|
||||
* **Domain Context**:
|
||||
- Remember that vet scans analyze software dependencies for security issues
|
||||
- Common entities include: packages, vulnerabilities, licenses, malware, scorecards
|
||||
- Explain technical terms that may be unfamiliar to users
|
||||
|
||||
@ -3,11 +3,12 @@ package cloud
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/cloud"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -178,7 +179,6 @@ func executeCreateKey() error {
|
||||
Desc: keyDescription,
|
||||
ExpiryInDays: keyExpiresIn,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -5,11 +5,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/oauth/api"
|
||||
"github.com/cli/oauth/device"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newCloudLoginCommand() *cobra.Command {
|
||||
@ -17,7 +19,7 @@ func newCloudLoginCommand() *cobra.Command {
|
||||
Use: "login",
|
||||
Short: "Login to SafeDep cloud for management tasks",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := executeDeviceAuthFlow()
|
||||
err := executeCloudLogin()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to login to the SafeDep cloud: %v", err)
|
||||
}
|
||||
@ -29,14 +31,24 @@ func newCloudLoginCommand() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func executeDeviceAuthFlow() error {
|
||||
func executeCloudLogin() error {
|
||||
token, err := executeDeviceAuthFlow()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute device auth flow: %w", err)
|
||||
}
|
||||
|
||||
return auth.PersistCloudTokens(token.Token,
|
||||
token.RefreshToken, tenantDomain)
|
||||
}
|
||||
|
||||
func executeDeviceAuthFlow() (*api.AccessToken, error) {
|
||||
code, err := device.RequestCode(http.DefaultClient,
|
||||
auth.CloudIdentityServiceDeviceCodeUrl(),
|
||||
auth.CloudIdentityServiceClientId(),
|
||||
[]string{"offline_access", "openid", "profile", "email"},
|
||||
device.WithAudience(auth.CloudIdentityServiceAudience()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to request device code: %w", err)
|
||||
return nil, fmt.Errorf("failed to request device code: %w", err)
|
||||
}
|
||||
|
||||
ui.PrintSuccess("Please visit %s and enter the code %s to authenticate",
|
||||
@ -49,9 +61,8 @@ func executeDeviceAuthFlow() error {
|
||||
DeviceCode: code,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to authenticate: %w", err)
|
||||
return nil, fmt.Errorf("failed to authenticate: %w", err)
|
||||
}
|
||||
|
||||
return auth.PersistCloudTokens(token.Token,
|
||||
token.RefreshToken, tenantDomain)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -31,10 +35,24 @@ func NewCloudCommand() *cobra.Command {
|
||||
|
||||
cmd.AddCommand(newCloudLoginCommand())
|
||||
cmd.AddCommand(newRegisterCommand())
|
||||
cmd.AddCommand(newQueryCommand())
|
||||
cmd.AddCommand(newPingCommand())
|
||||
cmd.AddCommand(newWhoamiCommand())
|
||||
cmd.AddCommand(newKeyCommand())
|
||||
cmd.AddCommand(newCloudQuickstartCommand())
|
||||
|
||||
queryCmd := newQueryCommand()
|
||||
queryCmd.PreRunE = requireAccessTokenCheck
|
||||
|
||||
pingCmd := newPingCommand()
|
||||
pingCmd.PreRunE = requireAccessTokenCheck
|
||||
|
||||
whoamiCmd := newWhoamiCommand()
|
||||
whoamiCmd.PreRunE = requireAccessTokenCheck
|
||||
|
||||
keyCmd := newKeyCommand()
|
||||
keyCmd.PreRunE = requireAccessTokenCheck
|
||||
|
||||
cmd.AddCommand(queryCmd)
|
||||
cmd.AddCommand(pingCmd)
|
||||
cmd.AddCommand(whoamiCmd)
|
||||
cmd.AddCommand(keyCmd)
|
||||
|
||||
cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||
if tenantDomain != "" {
|
||||
@ -44,3 +62,26 @@ func NewCloudCommand() *cobra.Command {
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func requireAccessTokenCheck(cmd *cobra.Command, args []string) error {
|
||||
// Check if token was obtained/refreshed 5 mins ago
|
||||
// If > 5 mins, check the access token expiry
|
||||
// else return
|
||||
if auth.ShouldCheckAccessTokenExpiry() {
|
||||
// Check if access token is expired
|
||||
// If expired (ok), refresh the session
|
||||
if ok, err := auth.IsAccessTokenExpired(); err != nil {
|
||||
tenantDomainPlaceholder := auth.TenantDomain()
|
||||
if tenantDomainPlaceholder == "" {
|
||||
tenantDomainPlaceholder = "<your-tenant-domain>"
|
||||
}
|
||||
|
||||
ui.PrintError("Automatic token refresh failed, please re-login using `vet cloud login --tenant %s`", tenantDomainPlaceholder)
|
||||
return fmt.Errorf("failed to check access token expiry: %w", err)
|
||||
} else if ok {
|
||||
ui.PrintMsg("Refreshing Access Token")
|
||||
return auth.RefreshCloudSession()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -3,11 +3,12 @@ package cloud
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/cloud"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newPingCommand() *cobra.Command {
|
||||
|
||||
@ -4,11 +4,12 @@ import (
|
||||
"errors"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/cloud/query"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
314
cmd/cloud/quickstart.go
Normal file
@ -0,0 +1,314 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
controltowerv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
|
||||
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/controltower/v1"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/cloud"
|
||||
)
|
||||
|
||||
func newCloudQuickstartCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "quickstart",
|
||||
Short: "Quick onboarding to SafeDep Cloud and cli setup",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := executeCloudQuickstart()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// executeCloudQuickstart executes an opinionated quick start flow for the user
|
||||
// with the goal of least friction on-boarding to SafeDep Cloud and configuring
|
||||
// the cli with everything required to start using SafeDep Cloud services.
|
||||
func executeCloudQuickstart() error {
|
||||
ui.PrintMsg("🚀 Starting SafeDep Cloud Quickstart...")
|
||||
ui.PrintMsg("👋 Hello! Let's get you onboarded..")
|
||||
|
||||
// This will execute cloud authentication flow and persist the cloud tokens
|
||||
// in the local config file.
|
||||
if err := quickStartAuthentication(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Here we create a connection to the control plane with cloud token. This
|
||||
// connection may not be multi-tenant because user may not have any tenants
|
||||
// yet.
|
||||
conn, err := quickStartCreateConnection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Here we check if the user has any tenants. If not, we create a new one.
|
||||
userInfo, err := quickStartTenantSetup(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Here we get the tenant from the user info. The tenant domain is stored
|
||||
// in the local config file.
|
||||
tenant, err := quickStartSetupTenantFromAccess(userInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintMsg("✅ Your tenant is set to: %s", tenant.GetDomain())
|
||||
|
||||
// Close the previous connection
|
||||
if err := conn.Close(); err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while closing cloud connection: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// Here we re-create the connection because we need a multi-tenant connection
|
||||
conn, err = quickStartCreateConnection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := quickStartAPIKeyCreation(conn, tenant); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintMsg("✅ All done!")
|
||||
ui.PrintMsg("")
|
||||
ui.PrintMsg("🎉 You are all set! You can now start using SafeDep Cloud")
|
||||
|
||||
// TODO: We need the ability to auto-detect the project name and version
|
||||
// and then use that to sync the results to SafeDep Cloud
|
||||
ui.PrintMsg("✨ Run `vet scan -D /path/to/code --report-sync` to scan your code and sync the results to SafeDep Cloud")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func quickStartSetupTenantFromAccess(userInfo *controltowerv1.GetUserInfoResponse) (*controltowerv1pb.Tenant, error) {
|
||||
if len(userInfo.GetAccess()) == 0 {
|
||||
ui.PrintError("❌ Oops! This is weird, you should have access to at least one tenant. Please contact support.")
|
||||
return nil, fmt.Errorf("no tenant access")
|
||||
}
|
||||
|
||||
// If user has access to multiple tenants
|
||||
// Ask user about which tenant they want to use if they have more than one
|
||||
var tenant *controltowerv1pb.Tenant
|
||||
if len(userInfo.GetAccess()) > 1 {
|
||||
// Print all tenants with index
|
||||
var tenantOptions []string
|
||||
ui.PrintMsg("🔍 You have access to the following tenants:")
|
||||
for idx, tenant := range userInfo.GetAccess() {
|
||||
ui.PrintMsg("%s", fmt.Sprintf(" - [%d] %s", idx, tenant.GetTenant().GetDomain()))
|
||||
tenantOptions = append(tenantOptions, tenant.GetTenant().GetDomain())
|
||||
}
|
||||
|
||||
// Ask user which tenant they want to use
|
||||
var tenantIndex int
|
||||
err := survey.AskOne(&survey.Select{
|
||||
Message: "🔍 Which tenant do you want to use?",
|
||||
Options: tenantOptions,
|
||||
}, &tenantIndex)
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while asking which tenant to use: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tenant = userInfo.GetAccess()[tenantIndex].GetTenant()
|
||||
} else {
|
||||
tenant = userInfo.GetAccess()[0].GetTenant()
|
||||
}
|
||||
|
||||
if err := auth.PersistTenantDomain(tenant.GetDomain()); err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while persisting your tenant domain: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
func quickStartAuthentication() error {
|
||||
ui.PrintMsg("🔑 Start by creating an account or sign-in to your existing account")
|
||||
|
||||
token, err := executeDeviceAuthFlow()
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while authenticating you: %s", err.Error())
|
||||
ui.PrintMsg("ℹ️ If you are using email and password, ensure your email is verified.")
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSuccess("✅ Successfully authenticated you!")
|
||||
|
||||
ui.PrintMsg("🔑 Saving your cloud credentials in your local config...")
|
||||
if err := auth.PersistCloudTokens(token.Token, token.RefreshToken, ""); err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while saving your cloud credentials: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
ui.PrintSuccess("✅ Successfully saved your cloud credentials!")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func quickStartCreateConnection() (*grpc.ClientConn, error) {
|
||||
conn, err := auth.ControlPlaneClientConnection("vet-cloud-quickstart")
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while creating cloud connection: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func quickStartTenantSetup(conn *grpc.ClientConn) (*controltowerv1.GetUserInfoResponse, error) {
|
||||
ui.PrintMsg("🔍 Checking if you have an existing tenant...")
|
||||
|
||||
userService, err := cloud.NewUserService(conn)
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while creating user service: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userInfo, err := userService.CurrentUserInfo()
|
||||
if err != nil {
|
||||
return quickStartCreateNewTenant(conn)
|
||||
}
|
||||
|
||||
ui.PrintMsg("✅ You are already registered with SafeDep Cloud")
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func quickStartCreateNewTenant(conn *grpc.ClientConn) (*controltowerv1.GetUserInfoResponse, error) {
|
||||
ui.PrintMsg("📝 Looks like you don't have an existing tenant. Let's create one for you...")
|
||||
|
||||
userName, domain, err := quickStartGetTenantInputs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
onboardingService, err := cloud.NewOnboardingService(conn)
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while creating onboarding service: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = onboardingService.Register(&cloud.RegisterRequest{
|
||||
Name: userName,
|
||||
Email: registerEmail,
|
||||
OrgName: "Quickstart Organization",
|
||||
OrgDomain: domain,
|
||||
})
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while registering your tenant: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ui.PrintSuccess("✅ Successfully created a new tenant!")
|
||||
ui.PrintMsg("🔑 Please wait while we get you onboarded...")
|
||||
|
||||
userService, err := cloud.NewUserService(conn)
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while creating user service: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userService.CurrentUserInfo()
|
||||
}
|
||||
|
||||
func quickStartGetTenantInputs() (string, string, error) {
|
||||
var userName string
|
||||
err := survey.AskOne(&survey.Input{
|
||||
Message: "👤 What should we call you?",
|
||||
Default: "John Doe",
|
||||
}, &userName)
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while asking for your name: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
autoDomain := fmt.Sprintf("quickstart-%s", time.Now().Format("20060102150405"))
|
||||
var domain string
|
||||
err = survey.AskOne(&survey.Input{
|
||||
Message: "📝 We have automatically generated a domain for you. Here is your chance to update",
|
||||
Default: autoDomain,
|
||||
}, &domain)
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while asking for your domain: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if domain == "" {
|
||||
domain = autoDomain
|
||||
}
|
||||
|
||||
return userName, domain, nil
|
||||
}
|
||||
|
||||
func quickStartAPIKeyCreation(conn *grpc.ClientConn, tenant *controltowerv1pb.Tenant) error {
|
||||
var createAPIKey bool
|
||||
err := survey.AskOne(&survey.Confirm{
|
||||
Message: "🔑 Do you want to create a new API key for this tenant?",
|
||||
Default: true,
|
||||
}, &createAPIKey)
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while asking if you want to create an API key: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if !createAPIKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
var showAPIKey bool
|
||||
err = survey.AskOne(&survey.Confirm{
|
||||
Message: "Would you like to see the API key in addition to configuring it?",
|
||||
Default: true,
|
||||
}, &showAPIKey)
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while asking about showing the API key: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
createApiKeyService, err := cloud.NewApiKeyService(conn)
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while creating the API key service: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
apiKey, err := createApiKeyService.CreateApiKey(&cloud.CreateApiKeyRequest{
|
||||
Name: fmt.Sprintf("Quick Start API Key: %s", time.Now().Format("20060102150405")),
|
||||
Desc: "This is a quick start API key created for you by vet",
|
||||
ExpiryInDays: 30,
|
||||
})
|
||||
if err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while creating the API key: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if err := auth.PersistApiKey(apiKey.Key, tenant.GetDomain()); err != nil {
|
||||
ui.PrintError("❌ Oops! Something went wrong while persisting the API key: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if showAPIKey {
|
||||
ui.PrintMsg("✅ Here is your API key: %s", text.BgGreen.Sprint(apiKey.Key))
|
||||
ui.PrintMsg("🔒 Your key will expire on: %s", apiKey.ExpiresAt.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
ui.PrintMsg("ℹ️ Your tenant domain is: %s", text.BgGreen.Sprint(tenant.GetDomain()))
|
||||
ui.PrintMsg("🔑 Please save this API key in a secure location, it will not be shown again.")
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -3,11 +3,12 @@ package cloud
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/cloud"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -60,7 +61,6 @@ func registerUserTenant() error {
|
||||
OrgName: registerOrgName,
|
||||
OrgDomain: registerOrgDomain,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -4,11 +4,12 @@ import (
|
||||
"fmt"
|
||||
|
||||
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/cloud"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newWhoamiCommand() *cobra.Command {
|
||||
|
||||
@ -3,6 +3,7 @@ package code
|
||||
import (
|
||||
"github.com/safedep/code/core"
|
||||
"github.com/safedep/code/lang"
|
||||
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
)
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
package code
|
||||
|
||||
import (
|
||||
"github.com/safedep/vet/internal/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/command"
|
||||
)
|
||||
|
||||
var languageCodes []string
|
||||
@ -10,7 +11,7 @@ var languageCodes []string
|
||||
func NewCodeCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "code",
|
||||
Short: "Analyze souce code",
|
||||
Short: "Analyze source code",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
|
||||
@ -4,12 +4,13 @@ import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/command"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/code"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/safedep/vet/pkg/storage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -81,7 +82,7 @@ func internalStartScan() error {
|
||||
},
|
||||
OnScanEnd: func() error {
|
||||
ui.StopSpinner()
|
||||
ui.PrintSuccess("Code scanning completed")
|
||||
ui.PrintSuccess("🚀 Code scanning completed. Run vet scan with code context using --code flag")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
72
cmd/doc/generate.go
Normal file
@ -0,0 +1,72 @@
|
||||
package doc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
var (
|
||||
// markdownOutDir is the output directory for markdown doc files
|
||||
markdownOutDir string
|
||||
|
||||
// manOutDir is the output directory for troff (man markup) doc files
|
||||
manOutDir string
|
||||
)
|
||||
|
||||
func newGenerateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate docs / manual artifacts",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// we specify the root (see, not parent) command since its the starting point for docs
|
||||
return runGenerateCommand(cmd.Root())
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVar(&markdownOutDir, "markdown", "", "The output directory for markdown doc files")
|
||||
cmd.PersistentFlags().StringVar(&manOutDir, "man", "", "The output directory for troff (man markup) doc files")
|
||||
|
||||
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// At least one of the output directory is required
|
||||
if markdownOutDir == "" && manOutDir == "" {
|
||||
return errors.New("no output directory specified, at least one of the output directory is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenerateCommand(rootCmd *cobra.Command) error {
|
||||
// If markdown directory is specified
|
||||
if markdownOutDir != "" {
|
||||
// Create Markdown Manual
|
||||
if err := doc.GenMarkdownTree(rootCmd, markdownOutDir); err != nil {
|
||||
return errors.Wrap(err, "failed to generate markdown manual")
|
||||
}
|
||||
|
||||
fmt.Println("Markdown manual doc created in: ", markdownOutDir)
|
||||
}
|
||||
|
||||
// If troff (man markup) directory is specified
|
||||
if manOutDir != "" {
|
||||
// Create Troff (man markup) Manual
|
||||
manHeader := &doc.GenManHeader{
|
||||
Title: "VET",
|
||||
Source: "SafeDep",
|
||||
Manual: "VET Manual",
|
||||
}
|
||||
|
||||
if err := doc.GenManTree(rootCmd, manHeader, manOutDir); err != nil {
|
||||
return errors.Wrap(err, "failed to generate man (troff) manual")
|
||||
}
|
||||
|
||||
fmt.Println("Troff (man markup) manual doc created in: ", manOutDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
17
cmd/doc/main.go
Normal file
@ -0,0 +1,17 @@
|
||||
package doc
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewDocCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "doc",
|
||||
Short: "Documentation generation internal utilities",
|
||||
Hidden: true, // Hide from vet public commands and docs itself, since its only build utility
|
||||
}
|
||||
|
||||
cmd.AddCommand(newGenerateCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -9,7 +9,7 @@ func NewPackageInspectCommand() *cobra.Command {
|
||||
Use: "inspect",
|
||||
Short: "Inspect an OSS package",
|
||||
Long: `Inspect an OSS package using deep inspection and analysis.
|
||||
This command will integrate with local and remote analysis services.`,
|
||||
This command will integrate with local and remote analysis services.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
|
||||
@ -9,21 +9,34 @@ import (
|
||||
|
||||
"buf.build/gen/go/safedep/api/grpc/go/safedep/services/malysis/v1/malysisv1grpc"
|
||||
malysisv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/malysis/v1"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/safedep/dry/adapters"
|
||||
"github.com/safedep/dry/api/pb"
|
||||
"github.com/safedep/dry/utils"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/analytics"
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/common/registry"
|
||||
vetutils "github.com/safedep/vet/pkg/common/utils"
|
||||
"github.com/safedep/vet/pkg/malysis"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/safedep/vet/pkg/reporter"
|
||||
)
|
||||
|
||||
var (
|
||||
malwareAnalysisPackageUrl string
|
||||
malwareAnalysisTimeout time.Duration
|
||||
malwareAnalysisReportJSON string
|
||||
malwareAnalysisReportOSV string
|
||||
malwareAnalysisNoWait bool
|
||||
|
||||
malwareReportOSVFinderName string
|
||||
malwareReportOSVContacts []string
|
||||
malwareReportOSVReferenceURL string
|
||||
malwareReportOSVUseRange bool
|
||||
)
|
||||
|
||||
func newPackageMalwareInspectCommand() *cobra.Command {
|
||||
@ -47,6 +60,18 @@ func newPackageMalwareInspectCommand() *cobra.Command {
|
||||
"Timeout for malware analysis")
|
||||
cmd.Flags().StringVar(&malwareAnalysisReportJSON, "report-json", "",
|
||||
"Path to save malware analysis report in JSON format")
|
||||
cmd.Flags().StringVar(&malwareAnalysisReportOSV, "report-osv", "",
|
||||
"Dir path to save malware analysis report in OSV format and ossf/malicious-packages format")
|
||||
cmd.Flags().BoolVar(&malwareAnalysisNoWait, "no-wait", false,
|
||||
"Do not wait for malware analysis to complete")
|
||||
cmd.Flags().StringVar(&malwareReportOSVFinderName, "report-osv-finder-name", "",
|
||||
"Finder name for malware analysis report in OSV format")
|
||||
cmd.Flags().StringSliceVar(&malwareReportOSVContacts, "report-osv-contacts", []string{},
|
||||
"Contacts for malware analysis report in OSV format (URL, email, etc.)")
|
||||
cmd.Flags().StringVar(&malwareReportOSVReferenceURL, "report-osv-reference-url", "",
|
||||
"Custom reference URL for malware analysis report (defaults to app.safedep.io)")
|
||||
cmd.Flags().BoolVar(&malwareReportOSVUseRange, "report-osv-with-ranges", false,
|
||||
"Use range-based versioning in OSV report (default: use explicit versions)")
|
||||
|
||||
_ = cmd.MarkFlagRequired("purl")
|
||||
|
||||
@ -54,6 +79,14 @@ func newPackageMalwareInspectCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
func executeMalwareAnalysis() error {
|
||||
analytics.TrackCommandInspectMalwareAnalysis()
|
||||
|
||||
err := auth.Verify()
|
||||
if err != nil {
|
||||
return fmt.Errorf("access to Malicious Package Analysis requires an API key. " +
|
||||
"For more details: https://docs.safedep.io/cloud/quickstart/")
|
||||
}
|
||||
|
||||
cc, err := auth.MalwareAnalysisClientConnection("malware-analysis")
|
||||
if err != nil {
|
||||
return err
|
||||
@ -66,17 +99,54 @@ func executeMalwareAnalysis() error {
|
||||
return err
|
||||
}
|
||||
|
||||
githubClient, err := adapters.NewGithubClient(adapters.DefaultGitHubClientConfig())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GitHub client: %v", err)
|
||||
}
|
||||
|
||||
versionResolver, err := registry.NewPackageVersionResolver(githubClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create package version resolver: %v", err)
|
||||
}
|
||||
|
||||
packageVersion := purl.PackageVersion()
|
||||
|
||||
// If package version is empty or latest replace it with actual literal latest version
|
||||
// Reference: https://github.com/safedep/vet/issues/446
|
||||
if packageVersion.GetVersion() == "" || packageVersion.GetVersion() == "latest" {
|
||||
ui.PrintMsg("Resolving package version")
|
||||
version, err := versionResolver.ResolvePackageLatestVersion(purl.Ecosystem(), purl.Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve package latest version: %v", err)
|
||||
}
|
||||
|
||||
ui.PrintSuccess("Resolved package version: %s", version)
|
||||
packageVersion.Version = version
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancelFun := context.WithTimeout(ctx, malwareAnalysisTimeout)
|
||||
|
||||
defer cancelFun()
|
||||
|
||||
// For GitHub Actions packages, we need to resolve the commit hash
|
||||
if packageVersion.GetPackage().GetEcosystem() == packagev1.Ecosystem_ECOSYSTEM_GITHUB_ACTIONS {
|
||||
ui.PrintMsg("Resolving commit hash for GitHub Actions package")
|
||||
|
||||
commitHash, err := resolveGitHubActionsCommitHash(ctx, packageVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve commit hash for GitHub Actions package: %v", err)
|
||||
}
|
||||
|
||||
ui.PrintSuccess("Resolved commit hash for GitHub Actions package: %s", commitHash)
|
||||
packageVersion.Version = commitHash
|
||||
}
|
||||
|
||||
analyzePackageResponse, err := service.AnalyzePackage(ctx, &malysisv1.AnalyzePackageRequest{
|
||||
Target: &malysisv1pb.PackageAnalysisTarget{
|
||||
PackageVersion: purl.PackageVersion(),
|
||||
PackageVersion: packageVersion,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to submit package for malware analysis: %v", err)
|
||||
}
|
||||
@ -84,14 +154,18 @@ func executeMalwareAnalysis() error {
|
||||
ui.PrintMsg("Submitted package for malware analysis with ID: %s",
|
||||
analyzePackageResponse.GetAnalysisId())
|
||||
|
||||
if malwareAnalysisNoWait {
|
||||
return nil
|
||||
}
|
||||
|
||||
ui.StartSpinner("Waiting for malware analysis to complete")
|
||||
var report *malysisv1pb.Report
|
||||
var verificationRecord *malysisv1pb.VerificationRecord
|
||||
|
||||
for {
|
||||
reportResponse, err := service.GetAnalysisReport(ctx, &malysisv1.GetAnalysisReportRequest{
|
||||
AnalysisId: analyzePackageResponse.GetAnalysisId(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get malware analysis report: %v", err)
|
||||
}
|
||||
@ -102,6 +176,7 @@ func executeMalwareAnalysis() error {
|
||||
|
||||
if reportResponse.GetStatus() == malysisv1.AnalysisStatus_ANALYSIS_STATUS_COMPLETED {
|
||||
report = reportResponse.GetReport()
|
||||
verificationRecord = reportResponse.GetVerificationRecord()
|
||||
break
|
||||
}
|
||||
|
||||
@ -116,29 +191,71 @@ func executeMalwareAnalysis() error {
|
||||
|
||||
ui.PrintSuccess("Malware analysis completed successfully")
|
||||
|
||||
err = renderToJSON(report)
|
||||
if malwareAnalysisReportJSON != "" {
|
||||
ui.PrintMsg("Generating JSON report")
|
||||
|
||||
err = writeJSONReport(report)
|
||||
if err != nil {
|
||||
ui.PrintError("Failed to render malware analysis report in JSON format: %v", err)
|
||||
}
|
||||
|
||||
return renderMalwareAnalysisReport(malwareAnalysisPackageUrl,
|
||||
analyzePackageResponse.GetAnalysisId(), report)
|
||||
}
|
||||
|
||||
func renderToJSON(report *malysisv1pb.Report) error {
|
||||
if malwareAnalysisReportJSON == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if malwareAnalysisReportOSV != "" {
|
||||
if !report.GetInference().GetIsMalware() {
|
||||
ui.PrintWarning("Report is not malware, skipping OSV report generation")
|
||||
return nil
|
||||
} else {
|
||||
ui.PrintMsg("Generating OSV report in: %s", malwareAnalysisReportOSV)
|
||||
|
||||
err = writeOSVReport(report)
|
||||
if err != nil {
|
||||
ui.PrintError("Failed to render malware analysis report in OSV format: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renderMalwareAnalysisReport(malwareAnalysisPackageUrl,
|
||||
analyzePackageResponse.GetAnalysisId(), report, verificationRecord)
|
||||
}
|
||||
|
||||
func writeOSVReport(report *malysisv1pb.Report) error {
|
||||
err := os.MkdirAll(malwareAnalysisReportOSV, 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory: %v", err)
|
||||
}
|
||||
|
||||
generator, err := malysis.NewOpenSSFMaliciousPackageReportGenerator(malysis.OpenSSFMaliciousPackageReportGeneratorConfig{
|
||||
Dir: malwareAnalysisReportOSV,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create OpenSSF malicious package report generator: %v", err)
|
||||
}
|
||||
|
||||
err = generator.GenerateReport(context.Background(), report, malysis.OpenSSFMaliciousPackageReportParams{
|
||||
FinderName: malwareReportOSVFinderName,
|
||||
Contacts: malwareReportOSVContacts,
|
||||
ReferenceURL: malwareReportOSVReferenceURL,
|
||||
UseRange: malwareReportOSVUseRange,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate OpenSSF malicious package report: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSONReport(report *malysisv1pb.Report) error {
|
||||
data, err := utils.ToPbJson(report, " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(malwareAnalysisReportJSON, []byte(data), 0644)
|
||||
return os.WriteFile(malwareAnalysisReportJSON, []byte(data), 0o644)
|
||||
}
|
||||
|
||||
func renderMalwareAnalysisReport(purl string, analysisId string, report *malysisv1pb.Report) error {
|
||||
func renderMalwareAnalysisReport(purl string, analysisId string,
|
||||
report *malysisv1pb.Report, vr *malysisv1pb.VerificationRecord,
|
||||
) error {
|
||||
ui.PrintMsg("Malware analysis report for package: %s", purl)
|
||||
|
||||
tbl := table.NewWriter()
|
||||
@ -147,9 +264,13 @@ func renderMalwareAnalysisReport(purl string, analysisId string, report *malysis
|
||||
|
||||
tbl.AppendHeader(table.Row{"Package URL", "Status", "Confidence"})
|
||||
|
||||
status := text.FgHiGreen.Sprint("SAFE")
|
||||
status := reporter.InfoBgText(" SAFE ")
|
||||
if report.GetInference().GetIsMalware() {
|
||||
status = text.FgHiRed.Sprint("MALWARE")
|
||||
if vr != nil && vr.IsMalware {
|
||||
status = reporter.CriticalBgText(" MALICIOUS ")
|
||||
} else {
|
||||
status = reporter.WarningBgText(" SUSPICIOUS ")
|
||||
}
|
||||
}
|
||||
|
||||
confidence := report.GetInference().GetConfidence().String()
|
||||
@ -159,8 +280,8 @@ func renderMalwareAnalysisReport(purl string, analysisId string, report *malysis
|
||||
tbl.Render()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(text.FgHiYellow.Sprintf("** The full report is available at: %s",
|
||||
reportVisualizationUrl(analysisId)))
|
||||
fmt.Println(reporter.WarningText(fmt.Sprintf("** The full report is available at: %s",
|
||||
reportVisualizationUrl(analysisId))))
|
||||
fmt.Println()
|
||||
|
||||
return nil
|
||||
@ -169,3 +290,21 @@ func renderMalwareAnalysisReport(purl string, analysisId string, report *malysis
|
||||
func reportVisualizationUrl(analysisId string) string {
|
||||
return malysis.ReportURL(analysisId)
|
||||
}
|
||||
|
||||
func resolveGitHubActionsCommitHash(ctx context.Context, packageVersion *packagev1.PackageVersion) (string, error) {
|
||||
gha, err := adapters.NewGithubClient(adapters.DefaultGitHubClientConfig())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create GitHub client: %v", err)
|
||||
}
|
||||
|
||||
parts := strings.Split(packageVersion.GetPackage().GetName(), "/")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid repository name: %s - should be in the format <owner>/<repo>", packageVersion.GetPackage().GetName())
|
||||
}
|
||||
|
||||
owner := parts[0]
|
||||
repo := parts[1]
|
||||
|
||||
return vetutils.ResolveGitHubRepositoryCommitSHA(ctx,
|
||||
gha, owner, repo, packageVersion.GetVersion())
|
||||
}
|
||||
|
||||
17
cmd/server/main.go
Normal file
@ -0,0 +1,17 @@
|
||||
package server
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
func NewServerCommand() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Start available servers",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(newMcpServerCommand())
|
||||
|
||||
return &cmd
|
||||
}
|
||||
195
cmd/server/mcp.go
Normal file
@ -0,0 +1,195 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"buf.build/gen/go/safedep/api/grpc/go/safedep/services/insights/v2/insightsv2grpc"
|
||||
"buf.build/gen/go/safedep/api/grpc/go/safedep/services/malysis/v1/malysisv1grpc"
|
||||
"github.com/safedep/dry/adapters"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/auth"
|
||||
"github.com/safedep/vet/mcp"
|
||||
"github.com/safedep/vet/mcp/server"
|
||||
"github.com/safedep/vet/mcp/tools"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
mcpServerSseServerAddr string
|
||||
mcpServerServerType string
|
||||
skipDefaultTools bool
|
||||
registerVetSQLQueryTool bool
|
||||
vetSQLQueryToolDBPath string
|
||||
registerPackageRegistryTool bool
|
||||
sseServerAllowedOrigins []string
|
||||
sseServerAllowedHosts []string
|
||||
)
|
||||
|
||||
func newMcpServerCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "mcp",
|
||||
Short: "Start the MCP server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := startMcpServer()
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to start server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&mcpServerSseServerAddr, "sse-server-addr", "localhost:9988", "The address to listen for SSE connections")
|
||||
cmd.Flags().StringVar(&mcpServerServerType, "server-type", "stdio", "The type of server to start (stdio, sse)")
|
||||
|
||||
cmd.Flags().StringSliceVar(
|
||||
&sseServerAllowedOrigins,
|
||||
"sse-allowed-origins",
|
||||
nil,
|
||||
"List of allowed origin prefixes for SSE connections. By default, we allow http://localhost:, http://127.0.0.1: and https://localhost:.",
|
||||
)
|
||||
cmd.Flags().StringSliceVar(
|
||||
&sseServerAllowedHosts,
|
||||
"sse-allowed-hosts",
|
||||
nil,
|
||||
"List of allowed hosts for SSE connections. By default, we allow localhost:9988, 127.0.0.1:9988 and [::1]:9988.",
|
||||
)
|
||||
|
||||
// We allow skipping default tools to allow for custom tools to be registered when the server starts.
|
||||
// This is useful for agents to avoid unnecessary tool registration.
|
||||
cmd.Flags().BoolVar(&skipDefaultTools, "skip-default-tools", false, "Skip registering default tools")
|
||||
|
||||
// Options to register sqlite3 query tool
|
||||
cmd.Flags().BoolVar(®isterVetSQLQueryTool, "sql-query-tool", false, "Register the vet report query by SQL tool (requires database path)")
|
||||
cmd.Flags().StringVar(&vetSQLQueryToolDBPath, "sql-query-tool-db-path", "", "The path to the vet SQLite3 database file")
|
||||
|
||||
// Options to register package registry tool
|
||||
cmd.Flags().BoolVar(®isterPackageRegistryTool, "package-registry-tool", false, "Register the package registry tool")
|
||||
|
||||
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
if registerVetSQLQueryTool && vetSQLQueryToolDBPath == "" {
|
||||
return fmt.Errorf("database path is required for SQL query tool")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func startMcpServer() error {
|
||||
driver, err := buildMcpDriver()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build MCP driver: %w", err)
|
||||
}
|
||||
|
||||
var mcpSrv server.McpServer
|
||||
switch mcpServerServerType {
|
||||
case "stdio":
|
||||
mcpSrv, err = server.NewMcpServerWithStdioTransport(server.DefaultMcpServerConfig())
|
||||
case "sse":
|
||||
config := server.DefaultMcpServerConfig()
|
||||
|
||||
// Override with user supplied config
|
||||
config.SseServerAddr = mcpServerSseServerAddr
|
||||
|
||||
// override origins and hosts defaults only if user explicitly set them.
|
||||
// When explicitly passed as cmd line args, cobra parses
|
||||
// --sse-allowed-hosts='' as empty slice. Otherwise if not provided,
|
||||
// sse-allowed-hosts will be nil.
|
||||
if sseServerAllowedOrigins != nil {
|
||||
config.SseServerAllowedOriginsPrefix = sseServerAllowedOrigins
|
||||
}
|
||||
if sseServerAllowedHosts != nil {
|
||||
config.SseServerAllowedHosts = sseServerAllowedHosts
|
||||
}
|
||||
|
||||
mcpSrv, err = server.NewMcpServerWithSseTransport(config)
|
||||
default:
|
||||
return fmt.Errorf("invalid server type: %s", mcpServerServerType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create MCP server: %w", err)
|
||||
}
|
||||
|
||||
if !skipDefaultTools {
|
||||
err = doRegisterDefaultTools(mcpSrv, driver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register default tools: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if registerVetSQLQueryTool {
|
||||
err = doRegisterVetSQLQueryTool(mcpSrv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register vet SQL query tool: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if registerPackageRegistryTool {
|
||||
err = doRegisterPackageRegistryTool(mcpSrv, driver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register package registry tool: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = mcpSrv.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start MCP server: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRegisterDefaultTools(mcpSrv server.McpServer, driver mcp.Driver) error {
|
||||
return tools.RegisterAll(mcpSrv, driver)
|
||||
}
|
||||
|
||||
func doRegisterVetSQLQueryTool(mcpSrv server.McpServer) error {
|
||||
tool, err := tools.NewVetSQLQueryTool(vetSQLQueryToolDBPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create vet SQL query tool: %w", err)
|
||||
}
|
||||
|
||||
return mcpSrv.RegisterTool(tool)
|
||||
}
|
||||
|
||||
func doRegisterPackageRegistryTool(mcpSrv server.McpServer, driver mcp.Driver) error {
|
||||
err := mcpSrv.RegisterTool(tools.NewPackageRegistryTool(driver))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register package registry tool: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMcpDriver() (mcp.Driver, error) {
|
||||
insightsConn, err := auth.InsightsV2CommunityClientConnection("vet-mcp-insights")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create insights client: %w", err)
|
||||
}
|
||||
|
||||
communityConn, err := auth.MalwareAnalysisCommunityClientConnection("vet-mcp-malware")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create community client: %w", err)
|
||||
}
|
||||
|
||||
insightsClient := insightsv2grpc.NewInsightServiceClient(insightsConn)
|
||||
malysisClient := malysisv1grpc.NewMalwareAnalysisServiceClient(communityConn)
|
||||
|
||||
githubAdapter, err := adapters.NewGithubClient(adapters.DefaultGitHubClientConfig())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create github client: %w", err)
|
||||
}
|
||||
|
||||
driver, err := mcp.NewDefaultDriver(insightsClient, malysisClient, githubAdapter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create MCP driver: %w", err)
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
60
cmd/server/mcp_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/safedep/vet/test"
|
||||
)
|
||||
|
||||
func TestMcpDriver(t *testing.T) {
|
||||
test.EnsureEndToEndTestIsEnabled(t)
|
||||
|
||||
driver, err := buildMcpDriver()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build MCP driver: %v", err)
|
||||
}
|
||||
|
||||
t.Run("malysis community service is accessible", func(t *testing.T) {
|
||||
report, err := driver.GetPackageVersionMalwareReport(context.Background(), &packagev1.PackageVersion{
|
||||
Package: &packagev1.Package{
|
||||
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
||||
Name: "express",
|
||||
},
|
||||
Version: "4.17.1",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, report)
|
||||
})
|
||||
|
||||
t.Run("insights community service is accessible", func(t *testing.T) {
|
||||
vulns, err := driver.GetPackageVersionVulnerabilities(context.Background(), &packagev1.PackageVersion{
|
||||
Package: &packagev1.Package{
|
||||
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
||||
Name: "express",
|
||||
},
|
||||
Version: "4.17.1",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, vulns)
|
||||
assert.NotEmpty(t, vulns)
|
||||
})
|
||||
|
||||
t.Run("package registry adapter is accessible", func(t *testing.T) {
|
||||
res, err := driver.GetPackageLatestVersion(context.Background(), &packagev1.Package{
|
||||
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
||||
Name: "express",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res)
|
||||
assert.Equal(t, "express", res.GetPackage().GetName())
|
||||
assert.Equal(t, packagev1.Ecosystem_ECOSYSTEM_NPM, res.GetPackage().GetEcosystem())
|
||||
assert.NotEmpty(t, res.GetPackage().GetName())
|
||||
})
|
||||
}
|
||||
@ -1,19 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/oauth/device"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safedep/vet/internal/connect"
|
||||
"github.com/safedep/vet/internal/ui"
|
||||
"github.com/safedep/vet/pkg/common/logger"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newConnectCommand() *cobra.Command {
|
||||
@ -143,7 +143,6 @@ func connectGithubWithDeviceFlow() (string, error) {
|
||||
ClientID: clientID,
|
||||
DeviceCode: code,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@ -1,33 +1,9 @@
|
||||
# vet docs overview
|
||||
# vet Documentation
|
||||
|
||||
This section contains documentation for `vet`. This documentation site is based on the Docusaurus framework.
|
||||
## Usage
|
||||
|
||||
## Getting Started
|
||||
`vet` user documentation is available at [https://docs.safedep.io/](https://docs.safedep.io/)
|
||||
|
||||
To start a local environment of this project docs, please do the following
|
||||
## Development
|
||||
|
||||
- Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/safedep/vet.git
|
||||
```
|
||||
|
||||
- Navigate to the guide directory
|
||||
|
||||
```bash
|
||||
cd vet/docs
|
||||
```
|
||||
|
||||
- Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
- Start the development server
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
- Navigate to [http://localhost:3000](http://localhost:3000) for accessing the `vet` documentation locally
|
||||
- [Storage](./storage.md)
|
||||
|
||||
40
docs/agent.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Agents
|
||||
|
||||
`vet` natively supports AI agents with MCP based integration for tools.
|
||||
|
||||
To get started, set an API key for the LLM you want to use. Example:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-...
|
||||
export ANTHROPIC_API_KEY=sk-...
|
||||
export GEMINI_API_KEY=AIza...
|
||||
```
|
||||
|
||||
> **Note:** You can also set the model to use with `OPENAI_MODEL_OVERRIDE`, `ANTHROPIC_MODEL_OVERRIDE` and `GEMINI_MODEL_OVERRIDE` environment variables to override the default model used by the agent.
|
||||
|
||||
## Fast Mode
|
||||
|
||||
All agents support a `--fast` flag to use a faster LLM model instead of a slower but more powerful reasoning model. This is only for influencing the default choice of model. It can be overridden by setting model provider specific environment variables such as `OPENAI_MODEL_OVERRIDE`, `ANTHROPIC_MODEL_OVERRIDE` and `GEMINI_MODEL_OVERRIDE`.
|
||||
|
||||
## Available Agents
|
||||
|
||||
### Query Agent
|
||||
|
||||
The query agent helps run query and analysis over vet's sqlite3 reporting database. To use it:
|
||||
|
||||
* Run a `vet` scan and generate report in sqlite3 format
|
||||
|
||||
```bash
|
||||
vet scan --insights-v2 -M package-lock.json --report-sqlite3 report.db
|
||||
```
|
||||
|
||||
**Note:** Agents only work with `--insights-v2`
|
||||
|
||||
* Start the query agent
|
||||
|
||||
```bash
|
||||
vet agent query --db report.db
|
||||
```
|
||||
|
||||
* Thats it! Start asking questions about the scan results.
|
||||
|
||||
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
BIN
docs/assets/vet-demo.gif
Normal file
|
After Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 489 KiB After Width: | Height: | Size: 489 KiB |
BIN
docs/assets/vet-logo-dark.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/assets/vet-logo-light.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
BIN
docs/assets/vet-mcp-cursor.png
Normal file
|
After Width: | Height: | Size: 719 KiB |
BIN
docs/assets/vet-mcp-vscode.png
Normal file
|
After Width: | Height: | Size: 624 KiB |
|
Before Width: | Height: | Size: 346 KiB After Width: | Height: | Size: 346 KiB |
BIN
docs/assets/vet-terminal.png
Normal file
|
After Width: | Height: | Size: 516 KiB |
@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
35
docs/doc-generate.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Doc Generate
|
||||
|
||||
Docs for `cmd/doc/` command
|
||||
|
||||
> [!NOTE]
|
||||
> This command is `HIDDEN` and not listed in the help output of `vet`. It is used to generate the documentation / manual for the `vet` command line application.
|
||||
|
||||
## doc
|
||||
|
||||
Documentation generation internal utilities
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for doc
|
||||
```
|
||||
|
||||
## doc generate
|
||||
|
||||
Generate docs / manual artifacts
|
||||
|
||||
```
|
||||
doc generate [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for generate
|
||||
--man string The output directory for troff (man markup) doc files
|
||||
--markdown string The output directory for markdown doc files
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> At least one of the output directory is required
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"label": "👀 Advanced Usage",
|
||||
"position": 6,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Advanced usage of vet for various workflows"
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
title: 🚫 Allow & Deny List
|
||||
draft: true
|
||||
---
|
||||
|
||||
# 🚫 Vet - Allow & Deny List
|
||||
|
||||
In this section we will leverage the [Exceptions](./exceptions) to configure and design the entire workflows with allow & deny list.
|
||||
@ -1,43 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: ✍️ Build Your Own Queries
|
||||
---
|
||||
|
||||
# ✍️ Build Your Own Queries (BYOQ)
|
||||
|
||||
## Query Workflow
|
||||
|
||||
Scanning a package manifest is a resource intensive process as it involves enriching package metadata by querying Insights API. However, filtering and reporting may be done multiple times on the same manifest. To speed up the process, we can dump the enriched data as JSON and load the same for filtering and reporting.
|
||||
|
||||
- Dump enriched JSON manifests to a directory (example)
|
||||
|
||||
```bash
|
||||
vet scan --lockfile /path/to/package-lock.json --json-dump-dir /tmp/dump
|
||||
vet scan -D /path/to/repository --json-dump-dir /tmp/dump-many
|
||||
```
|
||||
|
||||
- Load the enriched metadata for filtering and reporting
|
||||
|
||||
```bash
|
||||
vet query --from /tmp/dump --report-summary
|
||||
vet query --from /tmp/dump --filter 'scorecard.score.Maintained == 0'
|
||||
```
|
||||
|
||||
## Security Guardrails with Filters
|
||||
|
||||
A simple security guardrail (in CI) can be achieved using the filters. The `--filter-fail` argument tells the `Filter Analyzer` module to fail the command if any package matches the given filter.
|
||||
|
||||
- Example: If OpenSSF Scorecard not maintained project score is `0` then fail the build
|
||||
|
||||
```bash
|
||||
vet query --from /path/to/json-dump \
|
||||
--filter 'scorecard.scores.Maintained == 0' \
|
||||
--filter-fail
|
||||
```
|
||||
|
||||
- Subsequently, the command fails with `-1` exit code in case of match
|
||||
|
||||
```bash
|
||||
➜ vet git:(develop) ✗ echo $?
|
||||
255
|
||||
```
|
||||
@ -1,88 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: ❗ Exceptions
|
||||
---
|
||||
|
||||
# ❗ Vet - Exceptions Management
|
||||
|
||||
Any security scanning tool may produce
|
||||
|
||||
1. False positive
|
||||
2. Issues that are acceptable for a period of time
|
||||
3. Issues that are ignored permanently
|
||||
|
||||
:::info
|
||||
|
||||
To support exceptions, we introduce the exception model defined in [exception spec](https://github.com/safedep/vet/blob/main/api/exceptions_spec.proto)
|
||||
|
||||
:::
|
||||
|
||||
## Use-case
|
||||
|
||||
As a user of `vet` tool, I want to add all existing packages or package versions as `exceptions` to make the scanner and filter analyses to ignore them while reporting issues so that I can deploy `vet` as a security guardrail to prevent introducing new packages with security issues
|
||||
|
||||
This workflow will allow users to
|
||||
|
||||
1. Accept the current issues as backlog to be mitigated over time
|
||||
2. Deploy `vet` as a security gate in CI to prevent introducing new issues
|
||||
|
||||
### Security Risks
|
||||
|
||||
Exceptions management should handle the potential security risk of ignoring a package and its future issues. To mitigate this risk, we will ensure that issues can be ignored till an acceptable time window and not permanently.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Generate Exceptions File
|
||||
|
||||
- Run a scan and dump raw data to a directory
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/repo --json-dump-dir /path/to/dump
|
||||
```
|
||||
|
||||
- Use `vet query` to generate exceptions for all existing packages
|
||||
|
||||
```bash
|
||||
vet query --from /path/to/dump \
|
||||
--exceptions-generate /path/to/exceptions.yml \
|
||||
--exceptions-filter 'true' \ # Optional filter for packages to add
|
||||
--exceptions-till '2023-12-12'
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
`--exceptions-till` is parsed as `YYYY-mm-dd` and will generate a timestamp of `00:00:00` in UTC timezone for the date in RFC3339 format
|
||||
|
||||
:::
|
||||
|
||||
### Customize Exceptions File
|
||||
|
||||
The generated exceptions file will add all packages, matching optional filter, into the `exceptions.yml` file. This file should be reviewed and customised as required before using it.
|
||||
|
||||
### Use Exceptions to Ignore Specific Packages
|
||||
|
||||
An exceptions file can be passed as a global flag to `vet`. It will be used for various commands such as `scan` or `query`.
|
||||
|
||||
```bash
|
||||
./vet --exceptions /path/to/exceptions.yml scan -D /path/to/repo
|
||||
```
|
||||
|
||||
:::caution
|
||||
|
||||
Do not pass this flag while generating exceptions list in query workflow to avoid incorrect exception list generation
|
||||
|
||||
:::
|
||||
|
||||
## Behavior
|
||||
|
||||
- All exceptions rules are applied only on a `Package`
|
||||
- All comparisons will be case-insensitive except version
|
||||
- Only `version` can have a value of `*` matching any version
|
||||
- Exceptions are globally managed and will be shared across packages
|
||||
- Exempted packages will be ignored by all analysers and reporters
|
||||
- First match policy for exceptions matching
|
||||
|
||||
Anti-patterns that will NOT be implemented
|
||||
|
||||
- Exceptions will not be implemented for manifests because they will cause false negatives
|
||||
- Exceptions will not be created without an expiry to avoid future false negatives on the package
|
||||
@ -1,125 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 🔎 Filtering
|
||||
---
|
||||
|
||||
# 🔎 Filtering
|
||||
|
||||
Filter command helps solve the problem of visibility for OSS dependencies in an application. To support various requirements, we adopt a generic [expressions language](https://github.com/google/cel-spec) for flexible filtering.
|
||||
|
||||
## Example
|
||||
|
||||
- The scan will list only packages that use the `MIT` license.
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/repo \
|
||||
--report-summary=false \
|
||||
--filter 'licenses.exists(p, p == "MIT")'
|
||||
```
|
||||
|
||||
- Find dependencies that seems not very popular
|
||||
|
||||
```bash
|
||||
vet scan --lockfiles /path/to/pom.xml --report-summary=false \
|
||||
--filter='projects.exists(x, x.stars < 10)'
|
||||
```
|
||||
|
||||
- Find dependencies with a critical vulnerability
|
||||
|
||||
```bash
|
||||
vet scan --lockfiles /path/to/pom.xml --report-summary=false \
|
||||
--filter='vulns.critical.exists_one(x, true)'
|
||||
```
|
||||
|
||||
## Input
|
||||
|
||||
Filter expressions work on packages (aka. dependencies) and evaluates to a boolean result. The package is included in the results table if the expression evaluates to `true`.
|
||||
|
||||
- Filter expressions get the following input data to work with
|
||||
|
||||
| Variable | Content |
|
||||
|-------------|-------------------------------------------------------------|
|
||||
| `_` | The root variable, holding other variables |
|
||||
| `vulns` | Holds a map of vulnerabiliteis by severity |
|
||||
| `scorecard` | Holds OpenSSF scorecard |
|
||||
| `projects` | Holds a list of source projects associated with the package |
|
||||
| `licenses` | Holds a list of liceses in SPDX license code format |
|
||||
|
||||
:::tip
|
||||
|
||||
Refer to [filter input spec](https://github.com/safedep/vet/blob/main/api/filter_input_spec.proto) for detailed structure of input messages.
|
||||
|
||||
:::
|
||||
|
||||
## Expressions
|
||||
|
||||
Expressions are [CEL](https://github.com/google/cel-spec) statements. While
|
||||
CEL internals are not required, an [introductory](https://github.com/google/cel-spec/blob/master/doc/intro.md)
|
||||
knowledge of CEL will help formulating queries. Expressions are logical
|
||||
statements that evaluate to `true` or `false`.
|
||||
|
||||
### Example Queries
|
||||
|
||||
| Description | Query |
|
||||
|----------------------------------------------|--------------------------------------|
|
||||
| Find packages with a critical vulnerability | `vulns.critical.exists(x, true)` |
|
||||
| Find unmaintained packages as per OpenSSF SC | `scorecard.scores.Maintained == 0` |
|
||||
| Find packages with low stars | `projects.exists(x, x.stars < 10)` |
|
||||
| Find packages with GPL-2.0 license | `licenses.exists(x, x == "GPL-2.0")`
|
||||
|
||||
:::tip
|
||||
|
||||
Refer to [scorecard checks](https://github.com/ossf/scorecard#checks-1) for a list of checks available from OpenSSF Scorecards project.
|
||||
|
||||
:::
|
||||
|
||||
## How does the filter input JSON look like?
|
||||
|
||||
```json
|
||||
{
|
||||
"pkg": {
|
||||
"ecosystem": "npm",
|
||||
"name": "lodash.camelcase",
|
||||
"version": "4.3.0"
|
||||
},
|
||||
"vulns": {
|
||||
"all": [],
|
||||
"critical": [],
|
||||
"high": [],
|
||||
"medium": [],
|
||||
"low": []
|
||||
},
|
||||
"scorecard": {
|
||||
"scores": {
|
||||
"Binary-Artifacts": 10,
|
||||
"Branch-Protection": 0,
|
||||
"CII-Best-Practices": 0,
|
||||
"Code-Review": 8,
|
||||
"Dangerous-Workflow": 10,
|
||||
"Dependency-Update-Tool": 0,
|
||||
"Fuzzing": 0,
|
||||
"License": 10,
|
||||
"Maintained": 0,
|
||||
"Packaging": -1,
|
||||
"Pinned-Dependencies": 9,
|
||||
"SAST": 0,
|
||||
"Security-Policy": 10,
|
||||
"Signed-Releases": -1,
|
||||
"Token-Permissions": 0,
|
||||
"Vulnerabilities": 10
|
||||
}
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"name": "lodash/lodash",
|
||||
"type": "GITHUB",
|
||||
"stars": 55518,
|
||||
"forks": 6787,
|
||||
"issues": 464
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
"MIT"
|
||||
]
|
||||
}
|
||||
```
|
||||
@ -1,27 +0,0 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
title: 🚧 Path Exclusion
|
||||
---
|
||||
|
||||
# 🚧 Path Exclusion
|
||||
|
||||
`vet` supports path exclusions for scenarios where a directory is the scan target but certain path patterns within the directory should be excluded from scan. This is available only for the `scan` command.
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/target --exclude 'docs/*'
|
||||
```
|
||||
|
||||
- Multiple path patterns can be provided for exclusion while scanning a directory
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/target \
|
||||
--exclude 'docs/*' \
|
||||
--exclude 'sub/dir/path/*'
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
The exclusion pattern matches any path, directory or file. Internally it uses
|
||||
Go regexp [MatchString](https://pkg.go.dev/regexp#MatchString)
|
||||
|
||||
:::
|
||||
@ -1,66 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: 📜 Policy as Code
|
||||
---
|
||||
|
||||
# 📜 Policy as Code (PaC)
|
||||
|
||||
## What is Policy as Code?
|
||||
|
||||
The idea of treating policy as code / config / rules that can be evaluated by tools to make runtime decisions. Generally, policy as code approach reduces ***cost of compliance*** by automating grunt work required to audit for policy violations.
|
||||
|
||||
## Why use Policy as Code?
|
||||
|
||||
Manual verification and approval of new software components is, to put it simply, tedious, contentious, error prone and incomplete. It is not possible to manually analyses a 3rd party dependency and its transitive dependencies. We believe organizational software supply chain policies should be codified so that tools can be build to automatically evaluate every 3rd party artifact for compliance against policies.
|
||||
|
||||
Examples of software supply chain policy may include:
|
||||
|
||||
- Dependencies must not have known critical vulnerabilities
|
||||
- Dependencies must be maintained
|
||||
- Dependencies must not be new / must have an extensive user base
|
||||
- Publishers must follow security standards e.g. [SLSA](https://slsa.dev/)
|
||||
|
||||
## Filter Suite
|
||||
|
||||
A single filter is useful for identification of packages that meet some specific criteria. While it helps solve various use-cases, it is not entirely suitable for `security guardrails` where multiple filters may be required to express an organization's acceptable OSS usage policy.
|
||||
|
||||
- For example, an organization may define a filter to deny certain type of packages
|
||||
|
||||
1. Any package that has a high or critical vulnerability
|
||||
2. Any package that does not match acceptable OSS licenses
|
||||
3. Any package that has a low [OpenSSF scorecard score](https://github.com/ossf/scorecard)
|
||||
|
||||
- To express this policy, multiple filters are needed such as
|
||||
|
||||
```bash
|
||||
vulns.critical.exists(p, true) ||
|
||||
licenses.exists(p, (p != "MIT") && (p != "Apache-2.0")) ||
|
||||
(scorecard.scores.Maintained == 0)
|
||||
```
|
||||
|
||||
- To solve this problem, we introduce the concept of `Filter Suite`. It can be represented as an YAML file containing multiple filters to match:
|
||||
|
||||
```yaml
|
||||
name: Generic Filter Suite
|
||||
description: Example filter suite with canned filters
|
||||
filters:
|
||||
- name: critical-vuln
|
||||
value: |
|
||||
vulns.critical.exists(p, true)
|
||||
- name: safe-licenses
|
||||
value: |
|
||||
licenses.exists(p, (p != "MIT") && (p != "Apache-2.0"))
|
||||
- name: ossf-maintained
|
||||
value: |
|
||||
scorecard.scores.Maintained == 0
|
||||
```
|
||||
|
||||
- A scan or query operation can be invoked using the filter suite:
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/repo --filter-suite /path/to/filters.yml --filter-fail
|
||||
```
|
||||
|
||||
- The filter suite will be evaluated as
|
||||
- Ordered list of filters as given in the suite file
|
||||
- Stop on first rule match for a given package
|
||||
@ -1,65 +0,0 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
title: ⏩ Shell Completion
|
||||
---
|
||||
|
||||
# ⏩ Shell completion for vet
|
||||
|
||||
Command-line completion or Shell completion is a feature provided by shells like `bash` or `zsh` that lets you type commands in a fast and easy way. This functionality automatically fills in partially typed commands when the user press the `tab` key.
|
||||
|
||||
- To enable shell completion for `vet` for various shells follow the below steps
|
||||
|
||||
## 1. Identify your current environment shell
|
||||
|
||||
```bash
|
||||
❯ echo $SHELL
|
||||
|
||||
/bin/zsh
|
||||
```
|
||||
|
||||
## 2. Generate the completion command for your shell
|
||||
|
||||
```zsh
|
||||
❯ vet completion zsh -h
|
||||
|
||||
Generate the autocompletion script for the zsh shell.
|
||||
|
||||
If shell completion is not already enabled in your environment you will need
|
||||
to enable it. You can execute the following once:
|
||||
|
||||
echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
To load completions in your current shell session:
|
||||
|
||||
source <(vet completion zsh); compdef _vet vet
|
||||
|
||||
To load completions for every new session, execute once:
|
||||
|
||||
#### Linux:
|
||||
|
||||
vet completion zsh > "${fpath[1]}/_vet"
|
||||
|
||||
#### macOS:
|
||||
|
||||
vet completion zsh > $(brew --prefix)/share/zsh/site-functions/_vet
|
||||
|
||||
You will need to start a new shell for this setup to take effect.
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 3. Run the commands to setup completion
|
||||
|
||||
```zsh
|
||||
echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
source <(vet completion zsh); compdef _vet vet
|
||||
vet completion zsh > $(brew --prefix)/share/zsh/site-functions/_vet
|
||||
```
|
||||
|
||||
## 4. Open new shell and you can see the completion activated
|
||||
|
||||
```zsh
|
||||
❯ vet [tab]
|
||||
```
|
||||
|
||||

|
||||
@ -1,10 +0,0 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
title: 🏛️ Architecture
|
||||
---
|
||||
|
||||
# 🏛️ Vet Architecture
|
||||
|
||||
Here is a very high-level overview of `vet` architecture from 10,000 foot view to solve your Open Source Risks. Vet has been create with powerful design & architecture to be scalable, efficient and extendable.
|
||||
|
||||

|
||||
@ -1,12 +0,0 @@
|
||||
---
|
||||
sidebar_position: 17
|
||||
title: 🎊 Community
|
||||
---
|
||||
|
||||
# 🎊 Community
|
||||
|
||||
First of all, thank you so much for showing interest in `vet`, we appreciate it ❤️
|
||||
|
||||
**Join the server using the link - https://rebrand.ly/safedep-community**
|
||||
|
||||
[](https://rebrand.ly/safedep-community)
|
||||
@ -1,136 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
title: 🧩 Configuration
|
||||
draft: true
|
||||
---
|
||||
|
||||
# 🧩 Configuring Vet
|
||||
|
||||
`vet` comes with super powers 🚀, this section will help you to understand and explore some of them so that you can take your open source security to next level 😎
|
||||
|
||||
## Scanning
|
||||
|
||||
### Scanning Directories
|
||||
|
||||
- If you wanted to scan the whole directory & automatically parse the dependencies/lockfile, you can use the `-D` or `--directory` flag.
|
||||
|
||||
```bash
|
||||
vet scan -D your-code/directory/path/
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
If you do not specify any directory, by default it takes present working directory as the input.
|
||||
|
||||
:::
|
||||
|
||||
### Scanning Files
|
||||
|
||||
- If you wanted to scan the specific file `lockfile` you can use the `-L` or `--lockfiles` flag.
|
||||
|
||||
```bash
|
||||
vet scan -D your-code/directory/path/
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
If you do not specify any directory, by default it takes present working directory as the input.
|
||||
|
||||
:::
|
||||
|
||||
### Scanning Non-standard files
|
||||
|
||||
- Sometimes you might have non-standard filenames for the dependencies, lockfiles. You can scan them as a supported package manifest with a non-standard name using the following command
|
||||
|
||||
```bash
|
||||
vet scan --lockfiles /path/to/gradle-compileOnly.lock --lockfile-as gradle.lockfile
|
||||
```
|
||||
|
||||
### Scanning Multiple files
|
||||
|
||||
```bash
|
||||
vet scan --lockfiles /path/to/gradle.lockfile --lockfiles requirements.txt
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Scanning Parsers
|
||||
|
||||
`vet` currently has 10 scanning parsers for various dependencies formats including Go, Python, Java, etc.
|
||||
|
||||
```bash
|
||||
❯ vet scan parsers
|
||||
Available Lockfile Parsers
|
||||
==========================
|
||||
|
||||
[0] buildscript-gradle.lockfile
|
||||
[1] go.mod
|
||||
[2] gradle.lockfile
|
||||
[3] package-lock.json
|
||||
[4] Pipfile.lock
|
||||
[5] pnpm-lock.yaml
|
||||
[6] poetry.lock
|
||||
[7] pom.xml
|
||||
[8] requirements.txt
|
||||
[9] yarn.lock
|
||||
```
|
||||
|
||||
## Scan Options
|
||||
|
||||
### Silent scan
|
||||
|
||||
- `vet` supports silent scan to prevent rendering UI using the following command with `-s` or `--silent` flag
|
||||
|
||||
```bash
|
||||
vet scan -s --lockfiles demo-client-java/gradle.lockfile
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Scan concurrency
|
||||
|
||||
- By default it set to `5`, you can increase or decrease using the `--concurrency` or `-C` flag
|
||||
|
||||
```bash
|
||||
❯ vet scan -C 10 --lockfiles demo-client-java/gradle.lockfile
|
||||
Scanning packages ... done! [115 in 5.87s]
|
||||
Scanning manifests ... done! [1 in 5.87s]
|
||||
```
|
||||
|
||||
- You can see the difference between the above and below scan time with same file(s)
|
||||
|
||||
```bash
|
||||
❯ vet scan -C 1 --lockfiles demo-client-java/gradle.lockfile
|
||||
Scanning packages ... done! [115 in 10.567s]
|
||||
Scanning manifests ... done! [1 in 10.567s]
|
||||
```
|
||||
|
||||
### Scanning transitive dependencies
|
||||
|
||||
- You can perform the transitive dependencies scan by running the following command with `--transitive` flag
|
||||
|
||||
```bash
|
||||
vet scan --transitive --lockfiles demo-client-java/gradle.lockfile
|
||||
```
|
||||
|
||||

|
||||
|
||||
- As you can see the above scan has found issues across `201` libraries
|
||||
|
||||
### Configuring transitive dependencies depth level
|
||||
|
||||
- You can change the transitive dependencies scan depth by running the following command with `--transitive-depth` flag
|
||||
|
||||
```bash
|
||||
vet scan --transitive --transitive-depth 5 --lockfiles demo-client-java/gradle.lockfile
|
||||
```
|
||||
|
||||

|
||||
|
||||
- As you can see the above scan has found issues across `237` libraries
|
||||
|
||||
:::info
|
||||
|
||||
By default if you don't specify the flag it takes `2` as depth
|
||||
|
||||
:::
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"label": "🌐 Ecosystem",
|
||||
"position": 15,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "🚧 Work-in-Progress (WIP): SafeDep's vet ecosystem"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Artifactory Systems
|
||||
|
||||
🚧 Work-in-Progress (WIP)
|
||||
|
||||
## JFrog
|
||||
|
||||
## Nexus
|
||||
@ -1,7 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Developer Tooling
|
||||
|
||||
🚧 Work-in-Progress (WIP)
|
||||
@ -1,14 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
|
||||
# Gateways
|
||||
|
||||
🚧 Work-in-Progress (WIP)
|
||||
|
||||
## Kong API Gateway
|
||||
|
||||
## AWS API Gateway
|
||||
|
||||
## Traefik Gateway
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# IDE
|
||||
|
||||
🚧 Work-in-Progress (WIP)
|
||||
|
||||
## VSCode
|
||||
|
||||
## JetBrains
|
||||
|
||||
## Vim
|
||||
|
||||
## NeoVim
|
||||
@ -1,13 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Integrations
|
||||
|
||||
🚧 Work-in-Progress (WIP)
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
## Gitlab CI
|
||||
|
||||
## Bitbucket Pipelines
|
||||
@ -1,13 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Logging & Monitoring Systems
|
||||
|
||||
🚧 Work-in-Progress (WIP)
|
||||
|
||||
## Elastic Stack
|
||||
|
||||
## Splunk
|
||||
|
||||
## DataDog
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Service Mesh
|
||||
|
||||
🚧 Work-in-Progress (WIP)
|
||||
|
||||
## Istio - Service Mesh
|
||||
|
||||
## LinkerD - Service Mesh
|
||||
|
||||
## Cilium - Service Mesh
|
||||
|
||||
## Kong - Service Mesh
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Systems & Solutions
|
||||
|
||||
🚧 Work-in-Progress (WIP)
|
||||
|
||||
## Kubernetes
|
||||
|
||||
## Nomad
|
||||
|
||||
## AWS Fargate
|
||||
|
||||
## Google Cloud Run
|
||||
@ -1,26 +0,0 @@
|
||||
---
|
||||
sidebar_position: 20
|
||||
title: 🙋 FAQ
|
||||
---
|
||||
|
||||
# 🙋 FAQ - Vet
|
||||
|
||||
### How do I disable the stupid banner?
|
||||
|
||||
- Set environment variable `VET_DISABLE_BANNER=1`
|
||||
|
||||
### Something is wrong! How do I debug this thing?
|
||||
|
||||
- Run without the eye candy UI and enable log to file or to `stdout`.
|
||||
|
||||
Log to `stdout`:
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/repo -s -l- -v
|
||||
```
|
||||
|
||||
Log to file:
|
||||
|
||||
```bash
|
||||
vet scan -D /path/to/repo -l /tmp/vet.log -v
|
||||
```
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"label": "📖 Guides",
|
||||
"position": 8,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Guides for various use-cases"
|
||||
}
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: 🏄 Code Analysis
|
||||
---
|
||||
|
||||
# Code Analysis
|
||||
|
||||
:::note
|
||||
|
||||
EXPERIMENTAL: This feature is experimental and may introduce breaking changes.
|
||||
|
||||
:::
|
||||
|
||||
`vet` has a code analysis framework built on top of [tree-sitter](https://tree-sitter.github.io/tree-sitter/) parsers. The goal
|
||||
of this framework is to support multiple languages, source repositories (local and remote),
|
||||
and create a representation of code that can be analysed for common software
|
||||
supply chain security related use-cases such as
|
||||
|
||||
- Identify shadowed imports
|
||||
- Identify evidence of a dependency actually being used
|
||||
- Import reachability analysis
|
||||
- Function reachability analysis
|
||||
|
||||
:::warning
|
||||
|
||||
The code analysis framework is designed specifically to be simple, fast and
|
||||
not to be a full-fledged static analysis tool. It is currently in early stages
|
||||
of development and may not support all languages or maintain API compatibility.
|
||||
|
||||
:::
|
||||
|
||||
## Build a Code Analysis Database
|
||||
|
||||
- Analyse code and build a database for further analysis.
|
||||
|
||||
```bash
|
||||
vet code --db /tmp/code.db \
|
||||
--src /path/to/app \
|
||||
--imports /virtualenvs/app/lib/python3.11/site-packages \
|
||||
--lang python \
|
||||
create-db
|
||||
```
|
||||
|
||||
The above command does the following:
|
||||
|
||||
- Uses Python as the language for parsing source code
|
||||
- Analyses application code recursively in `/path/to/app`
|
||||
- Analyses dependencies in `/virtualenvs/app/lib/python3.11/site-packages`
|
||||
- Creates a database at `/tmp/code.db` for further analysis
|
||||
|
||||
## Manual Query Execution
|
||||
|
||||
Use [cayleygraph](https://cayley.gitbook.io/cayley/) to query the database.
|
||||
|
||||
```bash
|
||||
docker run -it -p 64210:64210 -v /tmp/code.db:/db cayleygraph/cayley -a /db -d bolt
|
||||
```
|
||||
|
||||
- Navigate to `http://127.0.0.1:64210` in your browser
|
||||
|
||||
### Query Examples
|
||||
|
||||
#### Dependency Graph
|
||||
|
||||
Build dependency graph for your application
|
||||
|
||||
```js
|
||||
g.V().Tag("source").out("imports").Tag("target").all()
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### Import Reachability
|
||||
|
||||
Check if a specific import is reachable in your application
|
||||
|
||||
```js
|
||||
g.V("app").followRecursive(g.M().out("imports")).is("six").all()
|
||||
```
|
||||
|
||||
- `app` is the application originating from `app.py`
|
||||
- `six` is a python module imported transitively
|
||||
|
||||
### Query API
|
||||
|
||||
Refer to [Gizmo Query Language](https://cayley.gitbook.io/cayley/query-languages/gizmoapi)
|
||||
for documentation on constructing custom queries.
|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Dependency Cost
|
||||
@ -1,71 +0,0 @@
|
||||
---
|
||||
draft: false
|
||||
title: 📦 Dependency Inventory
|
||||
---
|
||||
|
||||
# Dependency Inventory
|
||||
|
||||
In this guide, we will use CycloneDX `gradle` plugin to generate a software
|
||||
bill of material (SBOM) and scan it using `vet`.
|
||||
|
||||
## CycloneDX Plugin Integration
|
||||
|
||||
An official [plugin](https://github.com/CycloneDX/cyclonedx-gradle-plugin) can
|
||||
be used with build automation tools such as Gradle, Maven, etc. to generate
|
||||
Software Bill of Materials(SBOM) for a Java/Android/Kotlin projects.
|
||||
|
||||
### Gradle Plugin Integration
|
||||
|
||||
The gradle plugin for generating cyclonedx sbom file has to be integrated into
|
||||
the build script i.e. `build.gradle` file.
|
||||
|
||||
```groovy
|
||||
plugins {
|
||||
id 'org.cyclonedx.bom' version '1.10.0'
|
||||
}
|
||||
|
||||
cyclonedxBom {
|
||||
includeConfigs = ["runtimeClasspath"]
|
||||
skipConfigs = ["compileClasspath", "testCompileClasspath"]
|
||||
skipProjects = [rootProject.name, "yourTestSubProject"]
|
||||
projectType = "application"
|
||||
schemaVersion = "1.6"
|
||||
destination = file("build/reports")
|
||||
outputName = "bom"
|
||||
outputFormat = "json"
|
||||
includeBomSerialNumber = false
|
||||
includeLicenseText = false
|
||||
includeMetadataResolution = true
|
||||
componentVersion = "2.0.0"
|
||||
componentName = "my-component"
|
||||
}
|
||||
```
|
||||
|
||||
Based on requirements, `includeConfigs` and `skipConfigs` properties in
|
||||
`cyclonedxBom` can be modified to only include runtime, compile-time, or
|
||||
implementation dependencies in the sbom artifact(s). Additionaly, in
|
||||
a multi-build project, `skipProjects` property can be used to exclude
|
||||
dependency resolution for a sub-project, thus reducing the noise.
|
||||
|
||||
### SBOM Generation
|
||||
|
||||
Now, to generate sbom artifacts, do a clean build of the project using its
|
||||
respective build tool: `gradle cleanBuild -b build.gradle :cyclonedxBom`
|
||||
|
||||

|
||||
|
||||
After a successful build, all the artifacts shall be stored in `build/reports`
|
||||
path, present in the project root.
|
||||
|
||||
## Scan SBOMs using Vet
|
||||
|
||||
vet supports scanning of SBOM files in both SPDX and CycloneDX format.
|
||||
Depending upon the plugin and build tool being used, appropriate parsers can be
|
||||
used to scan the artifacts for a vulnerability report.
|
||||
|
||||
```
|
||||
vet scan --lockfiles build/reports/bom.json --lockfile-as bom-cyclonedx --report-markdown=report.md
|
||||
vet scan --lockfiles build/reports/bom.json --lockfile-as bom-spdx --report-markdown=report.md
|
||||
```
|
||||
|
||||

|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Dependency Scanning
|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Drift Analysis
|
||||
@ -1,63 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 🧪 GitHub Code Scanning
|
||||
---
|
||||
|
||||
# GitHub Code Scanning Integration
|
||||
|
||||
GitHub supports [uploading SARIF](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning)
|
||||
reports to enable repository and organization-wide visibility of security
|
||||
events across different tools. `vet` supports exporting policy violation
|
||||
reports as [SARIF](#) which can be uploaded to GitHub.
|
||||
|
||||
## Using SARIF Reports
|
||||
|
||||
To generate a SARIF report, use the `vet` command with the `--report-sarif` flag:
|
||||
|
||||
```shell
|
||||
vet scan -D /path/to/project --report-sarif /path/to/report.sarif
|
||||
```
|
||||
|
||||
## GitHub Action
|
||||
|
||||
`vet` has a GitHub Action to easy integration. Refer to [vet GitHub
|
||||
Action](../integrations/github-actions.md) for more details. The action
|
||||
produces a SARIF report which can be uploaded to GitHub.
|
||||
|
||||
Invoke `vet-action` to run `vet` in GitHub
|
||||
|
||||
```yaml
|
||||
- name: Run vet
|
||||
id: vet
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
uses: safedep/vet-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
Upload the SARIF report to GitHub
|
||||
|
||||
```yaml
|
||||
- name: Upload SARIF
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: ${{ steps.vet.outputs.report }}
|
||||
category: vet
|
||||
```
|
||||
|
||||
[Full Example](https://github.com/safedep/vet-action/blob/main/example/vet-ci.yml)
|
||||
|
||||
**Note:** `vet` will only include policy violations in the SARIF report.
|
||||
A policy must be provided to `vet` using `--filter` or `--filter-suite` flag
|
||||
during scan. This is automatically included if you are using `vet-action`.
|
||||
|
||||
## GitHub Code Scanning Alerts
|
||||
|
||||
Once the SARIF report is uploaded to GitHub, policy violations will be
|
||||
available in the GitHub Security tab. This provides a centralized view of
|
||||
policy violations across different repositories.
|
||||
|
||||

|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
draft: true
|
||||
---
|
||||
|
||||
# Health Status
|
||||