Compare commits

..

No commits in common. "main" and "v0.0.5-dev" have entirely different histories.

551 changed files with 1804 additions and 257885 deletions

View File

@ -1,24 +0,0 @@
{
"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"
]
}
}
}

View File

@ -1,10 +0,0 @@
---
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

View File

@ -1,15 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/docs"
schedule:
interval: "weekly"

View File

@ -1,42 +0,0 @@
name: SafeDep vet OSS suite
description: |
Customized filter suite for vet vetting vet.
tags:
- general
- safedep-vet
filters:
- name: critical-or-high-vulns
check_type: CheckTypeVulnerability
summary: Critical or high risk vulnerabilities were found
value: |
vulns.critical.exists(p, true) || vulns.high.exists(p, true)
- name: low-popularity
check_type: CheckTypePopularity
summary: Component popularity is low by Github stars count
value: |
projects.exists(p, (p.type == "GITHUB") && (p.stars < 10))
- name: risky-oss-licenses
check_type: CheckTypeLicense
summary: Risky OSS license was detected
value: |
licenses.exists(p, p == "GPL-2.0") ||
licenses.exists(p, p == "GPL-2.0-only") ||
licenses.exists(p, p == "GPL-3.0") ||
licenses.exists(p, p == "GPL-3.0-only") ||
licenses.exists(p, p == "BSD-3-Clause OR GPL-2.0")
- name: ossf-unmaintained
check_type: CheckTypeMaintenance
summary: Component appears to be unmaintained
value: |
scorecard.scores["Maintained"] == 0
- name: osv-malware
check_type: CheckTypeMalware
summary: Malicious (malware) component detected
value: |
vulns.all.exists(v, v.id.startsWith("MAL-"))
- name: ossf-dangerous-workflow
check_type: CheckTypeSecurityScorecard
summary: Component release pipeline appear to use dangerous workflows
value: |
scorecard.scores["Dangerous-Workflow"] == 0

View File

@ -1,138 +0,0 @@
name: CI
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
with:
go-version-file: go.mod
- name: Build and Test
run: |
go mod tidy
go build
go test -coverprofile=coverage.txt -v ./...
env:
VET_E2E: true
# Used to avoid rate limiting issue while running
# 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
with:
go-version-file: go.mod
check-latest: true
- name: Build vet
run: |
go mod tidy
go build
- name: Run E2E Scenarios
run: |
./test/scenarios/all.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run E2E Scenarios with Insights V2
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: |
./test/scenarios/all.sh
env:
E2E_INSIGHTS_V2: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# This will not be available when there is a PR from a forked repository
VET_API_KEY: ${{ secrets.SAFEDEP_CLOUD_API_KEY }}
VET_CONTROL_TOWER_TENANT_ID: ${{ secrets.SAFEDEP_CLOUD_TENANT_DOMAIN }}
build-container-test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout Source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Setup QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Build Multi-Platform Container Image (verification only)
run: |
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

View File

@ -13,18 +13,14 @@ name: "CodeQL"
on:
push:
branches: ["main"]
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: ["main"]
permissions:
contents: read
branches: [ "main" ]
jobs:
analyze:
if: "!contains(github.event.commits[0].message, '[noci]')"
timeout-minutes: 30
name: Analyze
runs-on: ubuntu-latest
permissions:
@ -35,34 +31,28 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ["go"]
language: [ 'go' ]
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34
with:
go-version-file: go.mod
check-latest: true
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
- name: Initialize CodeQL
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.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
- run: |
go mod tidy
go build
- run: |
go mod tidy
go build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
with:
category: "/language:${{matrix.language}}"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@ -1,156 +0,0 @@
name: Container Image Releaser
on:
push:
tags:
- "*"
branches:
- "main"
concurrency: ci-container-release
permissions:
contents: read
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
if: "!contains(github.event.commits[0].message, '[noci]')"
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744
with:
submodules: true
fetch-depth: 0
- name: Registry Login
uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup QEMU
uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55
- name: Build and Push Container Image
run: |
# 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"

View File

@ -1,21 +0,0 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
#
# 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"
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: "Checkout Repository"
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: "Dependency Review"
uses: actions/dependency-review-action@cc4f6536e38d1126c5e3b0683d469a14f23bfea4 # v3

View File

@ -1,65 +0,0 @@
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

View File

@ -1,27 +0,0 @@
name: Go Linter
on:
pull_request:
branches:
- main
permissions:
contents: read
pull-requests: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
with:
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
with:
version: latest
args: --issues-exit-code=1 --timeout=10m
only-new-issues: true

View File

@ -1,26 +1,19 @@
name: Release Automation
name: goreleaser
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
concurrency: ci-release-automation
- "*" # triggers only if push new tag version, like `0.8.4` or else
permissions:
contents: read
env:
OSX_CROSS_TOOLCHAIN_REPOSITORY: https://github.com/abhisek/osxcross
OSX_CROSS_MACOS_SDK_VERSION: "12.3"
jobs:
goreleaser:
timeout-minutes: 60
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
permissions:
contents: write # for goreleaser/goreleaser-action to create a GitHub release
contents: write # for goreleaser/goreleaser-action to create a GitHub release
packages: write # for goreleaser/goreleaser-action to publish docker images
runs-on: ubuntu-latest
env:
@ -34,9 +27,9 @@ jobs:
- uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2
- uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 # v2
- name: Set up Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
with:
go-version-file: go.mod
go-version: 1.19
check-latest: true
- name: ghcr-login
uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7 # v1
@ -44,62 +37,31 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install OSX Cross Compiler Build Tools
run: sudo apt-get install -y -qq build-essential clang gcc g++ gcc-mingw-w64 zlib1g-dev libmpc-dev libmpfr-dev libgmp-dev cmake libxml2-dev libssl-dev xz-utils gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
- name: Setup OSX Cross Compiler Tool Chain Environment
run: |
echo "OSXCROSS_DIR=$(dirname $GITHUB_WORKSPACE)/osxcross" >> $GITHUB_ENV
- name: Clone OSX Cross Compiler Tool Chain
run: git clone $OSX_CROSS_TOOLCHAIN_REPOSITORY $OSXCROSS_DIR
- name: Setup Cache for OSX Cross Compiler Tool Chain
id: osxcross-cache
uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f # v3
with:
key: ${{ runner.os }}-osxcross-${{ env.OSX_CROSS_MACOS_SDK_VERSION }}
path: |
${{ env.OSXCROSS_DIR }}/target/bin
- name: Build OSX Cross Compiler Tool Chain
if: steps.osxcross-cache.outputs.cache-hit != 'true'
run: |
cd $OSXCROSS_DIR
SDK_VERSION=$OSX_CROSS_MACOS_SDK_VERSION UNATTENDED=yes ./build.sh
- name: Add OSX Cross Compiler Tool Chain to Path
run: |
echo "$OSXCROSS_DIR/target/bin" >> $GITHUB_PATH
- name: Run GoReleaser
id: run-goreleaser
uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0
uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 # v4.1.0
with:
distribution: goreleaser
version: "~> v2"
args: release --clean
version: latest
args: release --rm-dist
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/
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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.
attestations: write # To write attestations
runs-on: ubuntu-latest
steps:
- name: Download dist folder
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: dist-artifacts
path: dist/
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0
with:
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
upload-assets: true
private-repository: true
- name: Attest build provenance (checksums)
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-checksums: dist/checksums.txt

View File

@ -1,63 +0,0 @@
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

View File

@ -1,60 +0,0 @@
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: "35 22 * * 0"
push:
branches: ["main"]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
steps:
- name: "Checkout code"
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecard on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 # v2.1.27
with:
sarif_file: results.sarif

View File

@ -3,23 +3,18 @@ on:
pull_request:
branches:
- main
permissions:
contents: read
jobs:
trufflehog:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout Source
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5
uses: actions/checkout@v2
with:
fetch-depth: '0'
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@8b6f55b592e46ac44a42dc3e3dee0ebcc0f56df5
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
base: main
head: HEAD

View File

@ -1,46 +0,0 @@
name: vet OSS Components
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
issues: write
pull-requests: write
security-events: write
jobs:
vet:
name: vet
runs-on: ubuntu-latest
steps:
- name: Checkout
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Enable Cloud Mode
run: echo "SAFEDEP_CLOUD_MODE=true" >> $GITHUB_ENV
- name: Override Cloud Mode if Actor is Dependabot
if: github.actor == 'dependabot[bot]'
run: echo "SAFEDEP_CLOUD_MODE=false" >> $GITHUB_ENV
- name: Override Cloud Mode if PR is from External Repository
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
run: echo "SAFEDEP_CLOUD_MODE=false" >> $GITHUB_ENV
- name: Run vet
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 }}

View File

@ -1,36 +0,0 @@
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

14
.gitignore vendored
View File

@ -5,11 +5,6 @@
*.so
*.dylib
# Test database files
*.db
*.sqlite
*.sqlite3
# Test binary, built with `go test -c`
*.test
@ -20,12 +15,5 @@
# vendor/
/vet
dist/
/.env.dev
.vscode/
# MacOS specific files
**/.DS_Store
# Auto-generated context files
CLAUDE.md

View File

@ -1,18 +0,0 @@
# 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

View File

@ -1,70 +1,18 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
version: 2
before:
hooks:
- go mod tidy
- go generate ./...
env:
- CGO_ENABLED=1
builds:
- id: linux
goos: [linux]
goarch: [amd64, arm64]
env:
- CGO_ENABLED=1
overrides:
- goos: linux
goarch: amd64
env:
- CC=x86_64-linux-gnu-gcc
- CXX=x86_64-linux-gnu-g++
- goos: linux
goarch: arm64
env:
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
- id: darwin
goos: [darwin]
goarch: [amd64, arm64]
env:
- CGO_ENABLED=1
- CC=o64-clang
- CXX=o64-clang++
- id: windows
goos: [windows]
goarch: [amd64]
env:
- CGO_ENABLED=1
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
release:
# for prerelease it doesn't build and distribute
prerelease: auto
universal_binaries:
- replace: true
brews:
- name: vet
homepage: https://safedep.io
description: "SafeDep vet is a tool for identifying open source software supply chain risks"
license: "Apache-2.0"
repository:
owner: safedep
name: homebrew-tap
branch: main
# TODO: Move to PR workflow once v1.17 is released
# branch: develop/vet
# pull_request:
# enabled: true
# base: main
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
archives:
- format: tar.gz
@ -76,19 +24,19 @@ archives:
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides:
- goos: windows
format: zip
- goos: windows
format: zip
checksum:
name_template: "checksums.txt"
algorithm: sha256
name_template: 'checksums.txt'
snapshot:
version_template: "{{ incpatch .Version }}-next"
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- '^docs:'
- '^test:'
# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json

View File

@ -1,74 +0,0 @@
{
"$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"
}
}
]
}

View File

@ -1,2 +0,0 @@
golang 1.25.1
gitleaks 8.16.4

23
.vscode/settings.json vendored
View File

@ -1,23 +0,0 @@
{
"[go]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"gopls": {
"usePlaceholders": true,
"completeUnimported": true,
"staticcheck": true,
"ui.semanticTokens": true,
"formatting.gofumpt": true,
},
"cSpell.words": [
"ineffassign",
"Infof",
"lockfiles",
"nolint",
"safedep"
]
}

View File

@ -1,102 +0,0 @@
# Contributing Guide
You can contribute to `vet` and help make it better. Apart from bug fixes,
features, we particularly value contributions in the form of:
- Documentation improvements
- Bug reports
- Using `vet` in your projects and providing feedback
## How to contribute
1. Fork the repository
2. Add your changes
3. Submit a pull request
## How to report a bug
Create a new issue and add the label "bug".
## How to suggest a new feature
Create a new issue and add the label "enhancement".
## Development workflow
When contributing changes to repository, follow these steps:
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.25.0+
### Install Dependencies
- Install [ASDF](https://asdf-vm.com/)
- Install the development tools
```bash
asdf plugin add golang
asdf plugin add gitleaks
asdf install
```
- Install git hooks (using Go toolchain)
```bash
go tool github.com/evilmartians/lefthook install
```
Install `golangci-lint`
```shell
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
```
### Build
Install build tools
```bash
make dev-setup
```
Generate code from API specs and build `vet`
```bash
make
```
Quick build without regenerating code from API specs
```bash
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
make test
```

View File

@ -1,45 +0,0 @@
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
# 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 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
ENTRYPOINT ["vet"]

View File

@ -1,12 +0,0 @@
vet is built and maintained by SafeDep with the help of the community.
https://safedep.io
Abhisek Datta
Email: abhisek@safedep.io
GitHub username: @abhisek
Affiliation: SafeDep
Nikhil Mittal
Email: nikhil.mittal641@gmail.com
GitHub username: @c0d3G33k
Affiliation: Chargebee

View File

@ -2,77 +2,32 @@ SHELL := /bin/bash
GITCOMMIT := $(shell git rev-parse HEAD)
VERSION := "$(shell git describe --tags --abbrev=0)-$(shell git rev-parse --short HEAD)"
all: quick-vet
all: clean setup vet
.PHONY: ent
ent:
go generate ./ent
.PHONY: filterv2-gen
filterv2-gen:
go generate ./pkg/analyzer/filterv2/...
generate: ent filterv2-gen
oapi-codegen-install:
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.10.1
protoc-install:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
dev-setup: 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
oapi-codegen -package controlplane -generate types ./api/cp-v1-trials.yml > ./gen/controlplane/trials.types.go
oapi-codegen -package controlplane -generate client ./api/cp-v1-trials.yml > ./gen/controlplane/trials.client.go
protoc-codegen:
protoc -I ./api \
--go_out=./gen/filterinput \
--go_opt=paths=source_relative \
./api/filter_input_spec.proto
protoc -I ./api \
--go_out=./gen/filtersuite \
--go_opt=paths=source_relative \
./api/filter_suite_spec.proto
protoc -I ./api \
--go_out=./gen/exceptionsapi \
--go_opt=paths=source_relative \
./api/exceptions_spec.proto
protoc -I ./api \
--go_out=./gen/models \
--go_opt=paths=source_relative \
./api/models.proto
protoc -I ./api \
--go_out=./gen/models \
--go_opt=paths=source_relative \
./api/insights_models.proto
protoc -I ./api \
--go_out=./gen/jsonreport \
--go_opt=paths=source_relative \
./api/json_report_spec.proto
protoc -I ./api \
--go_out=./gen/violations \
--go_opt=paths=source_relative \
./api/violations.proto
protoc -I ./api \
--go_out=./gen/checks \
--go_opt=paths=source_relative \
./api/checks.proto
setup:
mkdir -p out \
gen/insightapi \
gen/cpv1trials \
gen/cpv1 \
gen/syncv1 \
gen/filterinput \
gen/filtersuite \
gen/exceptionsapi \
gen/models \
gen/jsonreport \
gen/violations \
gen/checks
mkdir -p out gen/insightapi gen/controlplane gen/filterinput
GO_CFLAGS=-X main.commit=$(GITCOMMIT) -X main.version=$(VERSION)
GO_LDFLAGS=-ldflags "-w $(GO_CFLAGS)"
quick-vet:
go build ${GO_LDFLAGS}
vet: oapi-codegen protoc-codegen
go build ${GO_LDFLAGS}

590
README.md
View File

@ -1,582 +1,106 @@
<div align="center">
<img width="3024" height="1964" alt="image" src="./docs/assets/vet-terminal.png" />
# vet
<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>
`vet` is a tool for identifying risks in open source software supply chain. It
helps engineering and security teams to identify potential issues in their open
source dependencies and evaluate them against organizational policies.
<div align="center">
[![Go Report Card](https://goreportcard.com/badge/github.com/safedep/vet)](https://goreportcard.com/report/github.com/safedep/vet)
[![License](https://img.shields.io/github/license/safedep/vet)](https://github.com/safedep/vet/blob/main/LICENSE)
[![Release](https://img.shields.io/github/v/release/safedep/vet)](https://github.com/safedep/vet/releases)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/safedep/vet/badge)](https://api.securityscorecards.dev/projects/github.com/safedep/vet)
[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)
[![CodeQL](https://github.com/safedep/vet/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/safedep/vet/actions/workflows/codeql.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/safedep/vet.svg)](https://pkg.go.dev/github.com/safedep/vet)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/safedep/vet)
## TL;DR
</div>
> Ensure `$(go env GOPATH)/bin` is in your `$PATH`
---
## 🎯 Why vet?
> **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:
**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
## ⚡ Quick Start
**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
```
### 📥 **Direct Download**
See [releases](https://github.com/safedep/vet/releases) for the latest version.
### 🐹 **Go Install**
Install using `go get`
```bash
go install github.com/safedep/vet@latest
```
### 🐳 **Container Image**
Alternatively, look at [Releases](https://github.com/safedep/vet/releases) for
a pre-built binary for your platform. [SLSA Provenance](https://slsa.dev/provenance/v0.1) is published
along with each binary release.
Get a trial API key for [Insights API](https://safedep.io/docs/concepts/raya-data-platform-overview) access
```bash
# 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
vet auth trial --email john.doe@example.com
```
### ⚙️ **Verify Installation**
> A time limited trial API key will be sent over email.
Configure `vet` to use API Key to access [Insights API](https://safedep.io/docs/concepts/raya-data-platform-overview)
```bash
vet version
# Should display version and build information
vet auth configure
```
## 🎮 Advanced Usage
> Insights API is used to enrich OSS packages with meta-data for rich query and policy
> decisions
### 🔍 **Scanning Options**
<table>
<tr>
<td width="50%">
**📁 Directory Scanning**
Run `vet` to identify risks
```bash
# Scan current directory
vet scan
# Scan a given directory
vet scan -D /path/to/project
# Resolve and scan transitive dependencies
vet scan -D . --transitive
vet scan -D /path/to/repository
```
**📄 Manifest Files**
or scan a specific (supported) package manifest
```bash
# 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
vet scan --lockfiles /path/to/pom.xml
vet scan --lockfiles /path/to/requirements.txt
vet scan --lockfiles /path/to/package-lock.json
```
</td>
<td width="50%">
> Use `vet scan parsers` to list supported package manifest parsers
**🐙 GitHub Integration**
The default scan uses an opinionated [Console Reporter](#) which presents
a summary of findings per package manifest. Thats NOT about it. Read more for
expression based filtering and policy evaluation.
## Filtering
Find dependencies that seems not very popular
```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
vet scan --lockfiles /path/to/pom.xml --report-console=false \
--filter='projects.exists(x, x.stars < 10)'
```
**📦 Artifact Scanning**
Find dependencies with a critical vulnerability
```bash
# 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
vet scan --lockfiles /path/to/pom.xml --report-console=false \
--filter='vulns.critical.exists_one(x, true)'
```
</td>
</tr>
</table>
> Use filtering along with `query` command for offline slicing and dicing of
> enriched package manifests. Read [filtering guide](docs/filtering.md)
### 🎯 **Policy Enforcement Examples**
[Common Expressions Language](https://github.com/google/cel-spec) is used to
evaluate filters on packages. Learn more about [filtering with vet](docs/filtering.md).
Look at [filter input spec](api/filter_input_spec.proto) on attributes
available to the filter expression.
```bash
# Security-first scanning
vet scan -D . \
--filter 'vulns.critical.exists(p, true) || vulns.high.exists(p, true)' \
--filter-fail
## Policy Evaluation
# License compliance
vet scan -D . \
--filter 'licenses.contains_license("GPL-3.0")' \
--filter-fail
TODO
# OpenSSF Scorecard requirements
vet scan -D . \
--filter 'scorecard.scores.Maintained < 5' \
--filter-fail
## FAQ
# Popularity-based filtering
vet scan -D . \
--filter 'projects.exists(p, p.type == "GITHUB" && p.stars < 50)' \
--filter-fail
```
### How do I disable the stupid banner?
### 🔧 **SBOM Support**
Set environment variable `VET_DISABLE_BANNER=1`
```bash
# Scan a CycloneDX SBOM
vet scan -M sbom.json --type bom-cyclonedx
### Can I use this tool without an API Key for Insight Service?
# Scan a SPDX SBOM
vet scan -M sbom.spdx.json --type bom-spdx
Probably no. All useful data (enrichments) for a detected package comes from
a backend service. The service is rate limited with quotas to prevent abuse.
# Generate SBOM output
vet scan -D . --report-cdx=output.sbom.json
Look at `api/insights-v1.yml`. It contains the contract expected for Insights
API. You can perhaps consider rolling out your own to avoid dependency with our
backend.
# Package URL scanning
vet scan --purl pkg:npm/lodash@4.17.21
```
## References
### 📊 **Query Mode & Data Persistence**
* https://github.com/google/osv-scanner
For large codebases and repeated analysis:
```bash
# 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
```
## 📊 Reporting
**vet** generate reports that are tailored for different stakeholders:
### 📋 **Report Formats**
<table>
<tr>
<td width="30%"><strong>🔍 For Security Teams</strong></td>
<td width="70%">
```bash
# 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
```
</td>
</tr>
<tr>
<td><strong>📖 For Developers</strong></td>
<td>
```bash
# Markdown reports for PRs
vet scan -D . --report-markdown=report.md
# Console summary (default)
vet scan -D . --report-summary
```
</td>
</tr>
<tr>
<td><strong>🏢 For Compliance</strong></td>
<td>
```bash
# SBOM generation
vet scan -D . --report-cdx=sbom.json
# Dependency graphs
vet scan -D . --report-graph=dependencies.dot
```
</td>
</tr>
</table>
### 🎯 **Report Examples**
```bash
# 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
```
### 🤖 **MCP Server**
**vet** can be used as an MCP server to vet open source packages from AI suggested code.
```bash
# Start the MCP server with SSE transport
vet server mcp --server-type sse
```
For more details, see [vet MCP Server](./docs/mcp.md) documentation.
### 🤖 **Agents**
See [vet Agents](./docs/agent.md) documentation for more details.
## 🛡️ Malicious Package Detection
**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.
- 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
### 🚀 **Quick Setup**
> Malicious package detection requires an API key for [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis).
```bash
# One-time setup
vet cloud quickstart
# Enable malware scanning
vet scan -D . --malware
# Query for known malicious packages without API key
vet scan -D . --malware-query
```
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**
[![Discord](https://img.shields.io/discord/1090352019379851304?color=7289da&label=Discord&logo=discord&logoColor=white)](https://rebrand.ly/safedep-community)
[![GitHub Discussions](https://img.shields.io/badge/GitHub-Discussions-green?logo=github)](https://github.com/safedep/vet/discussions)
[![Twitter Follow](https://img.shields.io/twitter/follow/safedepio?style=social)](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**
[![Star History Chart](https://api.star-history.com/svg?repos=safedep/vet&type=Date)](https://star-history.com/#safedep/vet&Date)
### 🙏 **Built With Open Source**
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" />

View File

@ -1,69 +0,0 @@
// 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)
}

View File

@ -1,165 +0,0 @@
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
}

View File

@ -1,17 +0,0 @@
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"])
}
})
}

View File

@ -1,145 +0,0 @@
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
}

View File

@ -1,45 +0,0 @@
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
}

View File

@ -1,279 +0,0 @@
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)
}

View File

@ -1,156 +0,0 @@
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
}

View File

@ -1,164 +0,0 @@
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
}

View File

@ -1,29 +0,0 @@
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
}

View File

@ -1,651 +0,0 @@
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]
}

View File

@ -1,280 +0,0 @@
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")
}

View File

@ -1,17 +0,0 @@
syntax = "proto3";
option go_package = "github.com/safedep/vet/gen/checks";
enum CheckType {
CheckTypeUnknown = 0;
CheckTypeVulnerability = 1;
CheckTypeMalware = 2;
CheckTypePopularity = 3;
CheckTypeMaintenance = 4;
CheckTypeSecurityScorecard = 5;
CheckTypeLicense = 6;
reserved 7 to 99;
CheckTypeOther = 100;
}

123
api/cp-v1-trials.yml Normal file
View File

@ -0,0 +1,123 @@
openapi: 3.0.2
info:
title: SafeDep Control Plane API for Trials Registration
contact:
name: SafeDep API
url: 'https://safedep.io'
description: |
Trials API provide a way for obtaining an API Key for data plane service access
using an Email Address. Trials is different from Registrations as the later
allows full access to the control plane while Trials is meant to allow access
only to a time bounded (expirable) API key for quick evaluation of tools.
version: 0.0.1
servers:
- url: 'https://{apiHost}/{apiBase}'
variables:
apiHost:
default: api.safedep.io
apiBase:
default: control-plane/v1
tags:
- name: Control Plane
description: Control Plane API
paths:
/trials:
post:
description: |
Register a trial user to obtain an expirable API Key. The API key will
be generated and sent to the user over Email to ensure validity and access
to the email by the requester. System defined limits will be applied to
maximum number of trial API keys that can be generated for an email.
operationId: registerTrialUser
tags:
- Control Plane
requestBody:
description: Trial registration request
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TrialRequest'
responses:
'201':
description: Successfully created an API key request
content:
application/json:
schema:
$ref: '#/components/schemas/TrialResponse'
'403':
description: Access to the API is denied
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'429':
description: Rate limit block
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'500':
description: Failed due to internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
components:
schemas:
ApiError:
type: object
properties:
message:
type: string
description: A descriptive message about the error meant for developer consumption
type:
type: string
description: An optional service or domain specific error group
enum:
- invalid_request
- operation_failed
- internal_error
code:
type: string
description: An error code identifying the error
enum:
- api_guard_invalid_credentials
- api_guard_rate_limit_exceeded
- api_guard_unauthorized
- api_guard_error
- app_generic_error
- app_security_error
- app_insufficient_parameters
- app_feature_not_enabled
- app_package_version_not_found
params:
type: object
description: Optional error specific attributes
additionalProperties:
type: object
properties:
key:
type: string
value:
type: string
TrialRequest:
type: object
properties:
email:
type: string
format: email
required:
- email
TrialResponse:
type: object
properties:
id:
type: string
minLength: 6
maxLength: 512
description: The ID of the trial registration request created in the system
expires_at:
type: string
format: date-time
description: The expiry time of the API key

View File

@ -1,18 +0,0 @@
syntax = "proto3";
option go_package = "github.com/safedep/vet/gen/exceptionsapi";
message Exception {
string id = 1;
string ecosystem = 2;
string name = 3;
string version = 4;
string expires = 5;
string pattern = 6; // To be used for special cases
}
message ExceptionSuite {
string name = 1;
string description = 2;
repeated Exception exceptions = 3;
}

View File

@ -3,27 +3,48 @@ syntax = "proto3";
/* Specifcations for filter input that can be used for query by CEL */
option go_package = "github.com/safedep/vet/gen/filterinput";
import "insights_models.proto";
// Only hold vulnerability IDs
message FilterInputVulnerabilities {
repeated InsightVulnerability all = 1;
repeated InsightVulnerability critical = 2;
repeated InsightVulnerability high = 3;
repeated InsightVulnerability medium = 4;
repeated InsightVulnerability low = 5;
message Vulnerability {
string id = 1; // OSV ID
string cve = 2; // CVE ID
}
message FilterInputPackageVersion {
// Only hold vulnerability IDs
message Vulnerabilities {
repeated Vulnerability all = 1;
repeated Vulnerability critical = 2;
repeated Vulnerability high = 3;
repeated Vulnerability medium = 4;
repeated Vulnerability low = 5;
}
// OpenSSF Scorecard
message Scorecard {
map<string, float> scores = 1;
}
enum ProjectType {
UNKNOWN = 0;
GITHUB = 1;
}
message ProjectInfo {
string name = 1;
ProjectType type = 2;
int32 stars = 3;
int32 forks = 4;
int32 issues = 5;
}
message PackageVersion {
string ecosystem = 1;
string name = 2;
string version = 3;
}
message FilterInput {
FilterInputPackageVersion pkg = 1;
FilterInputVulnerabilities vulns = 2;
InsightScorecard scorecard = 3;
repeated InsightProjectInfo projects = 4;
PackageVersion pkg = 1;
Vulnerabilities vulns = 2;
Scorecard scorecard = 3;
repeated ProjectInfo projects = 4;
repeated string licenses = 5;
}

View File

@ -1,22 +0,0 @@
syntax = "proto3";
option go_package = "github.com/safedep/vet/gen/filtersuite";
import "checks.proto";
message Filter {
string name = 1;
string value = 2;
CheckType check_type = 3;
string summary = 4;
string description = 5;
repeated string references = 6;
repeated string tags = 7;
}
message FilterSuite {
string name = 1;
string description = 2;
repeated Filter filters = 3;
repeated string tags = 4;
}

View File

@ -1,56 +0,0 @@
syntax = "proto3";
option go_package = "github.com/safedep/vet/gen/models";
message InsightVulnerabilitySeverity {
enum Type {
UNKNOWN_TYPE = 0;
CVSSV2 = 1;
CVSSV3 = 2;
}
enum Risk {
UNKNOWN_RISK = 0;
LOW = 1;
MEDIUM = 2;
HIGH = 3;
CRITICAL = 4;
}
Type type = 1;
string score = 2; // Score based on type (usually the CVSS metric)
Risk risk = 3;
}
message InsightVulnerability {
string id = 1; // OSV ID
string cve = 2; // CVE ID. DO NOT USE THIS outside vet. Its used for internal legacy reason
string title = 3;
repeated string aliases = 4; // Other IDs for same vuln in different databases
repeated InsightVulnerabilitySeverity severities = 5;
}
message InsightLicenseInfo {
string id = 1; // SPDX license ID
}
message InsightScorecard {
map<string, float> scores = 1;
float score = 2;
}
message InsightProjectInfo {
enum Type {
UNKNOWN = 0;
GITHUB = 1;
}
string name = 1;
Type type = 2;
int32 stars = 3;
int32 forks = 4;
int32 issues = 5;
string url = 6;
}

View File

@ -1,111 +0,0 @@
syntax = "proto3";
option go_package = "github.com/safedep/vet/gen/jsonreportspec";
import "models.proto";
import "insights_models.proto";
import "violations.proto";
enum RemediationAdviceType {
UnknownAdviceType = 0;
UpgradePackage = 1;
AlternatePopularPackage = 2;
AlternateSecurePackage = 3;
}
message RemediationAdvice {
RemediationAdviceType type = 1;
Package package = 2;
string target_package_name = 3;
string target_package_version = 4;
string target_alternate_package_name = 5;
string target_alternate_package_version = 6;
}
// We are introducing the concept of Threat as a reporting entity so
// that we can report threats like lockfile poisoning using a standard schema.
// But why do we need threats? Why not just use vet's paradigm of policy over
// enriched packages? The reason is, there are threats that are applicable in
// an environment, against a manifest or other entities or even group of entities.
// Hence it is required to introduce a threat as a reporting entity so that external
// tools can consume vet's reports and take actions based on the threats.
message ReportThreat {
enum Confidence {
UnknownConfidence = 0;
High = 1;
Medium = 2;
Low = 3;
}
enum Source {
UnknownSource = 0;
CWE = 1;
}
enum SubjectType {
UnknownSubject = 0;
Package = 1;
Manifest = 2;
}
enum ReportThreatId {
UnknownReportThreatId = 0;
LockfilePoisoning = 1;
}
ReportThreatId id = 1;
string instanceId = 2; // Unique threat instance ID per (ID, SubjectType, Subject) tuple
string message = 3;
SubjectType subject_type = 4;
string subject = 5;
Confidence confidence = 6;
Source source = 7;
string source_id = 8;
}
message PackageManifestReport {
string id = 1;
Ecosystem ecosystem = 2;
string path = 3;
repeated ReportThreat threats = 4;
string display_path = 5;
string source_type = 6;
string namespace = 7;
}
// PackageReport represents the first class entity for which we have different type
// of reporting information
message PackageReport {
Package package = 1;
// The manifests identified by IDs where this package belongs to
repeated string manifests = 2;
repeated Violation violations = 3;
repeated RemediationAdvice advices = 4;
// Insights data
repeated InsightVulnerability vulnerabilities = 5;
repeated InsightLicenseInfo licenses = 6;
repeated InsightProjectInfo projects = 8;
// Threats
repeated ReportThreat threats = 7;
}
message ReportMeta {
string tool_name = 1;
string tool_version = 2;
string created_at = 3;
}
message Report {
ReportMeta meta = 1;
repeated PackageManifestReport manifests = 2;
repeated PackageReport packages = 3;
}

View File

@ -1,35 +0,0 @@
syntax = "proto3";
option go_package = "github.com/safedep/vet/gen/models";
// Core data models on which `vet` operations. This should eventually
// become source of truth and we should remove the model definitions in
// Go code `models.go` and instead generate code from here
enum Ecosystem {
UNKNOWN_ECOSYSTEM = 0;
Maven = 1;
RubyGems = 2;
Go = 3;
Npm = 4;
PyPI = 5;
Cargo = 6;
NuGet = 7;
Packagist = 8;
Hex = 9;
Pub = 10;
CycloneDxSBOM = 11;
SpdxSBOM = 12;
}
message Package {
Ecosystem ecosystem = 1;
string name = 2;
string version = 3;
}
message PackageManifest {
Ecosystem ecosystem = 1;
string path = 2;
repeated Package packages = 3;
}

View File

@ -1,15 +0,0 @@
syntax = "proto3";
option go_package = "github.com/safedep/vet/gen/violations";
import "models.proto";
import "checks.proto";
import "filter_suite_spec.proto";
message Violation {
CheckType check_type = 1;
Package package = 2;
Filter filter = 3;
map<string, string> extra = 4;
}

111
auth.go
View File

@ -1,31 +1,36 @@
package main
import (
"errors"
"fmt"
"os"
"syscall"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
"golang.org/x/term"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/command"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/common/logger"
)
var authTenantDomain string
var (
authInsightApiBaseUrl string
authControlPlaneApiBaseUrl string
authTrialEmail string
)
func newAuthCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Configure vet authentication",
Short: "Configure and verify Insights API authentication",
RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("a valid sub-command is required")
fmt.Printf("You must choose an appropriate command: configure, verify\n")
os.Exit(1)
return nil
},
}
cmd.AddCommand(configureAuthCommand())
cmd.AddCommand(verifyAuthCommand())
cmd.AddCommand(trialsRegisterCommand())
return cmd
}
@ -34,73 +39,73 @@ func configureAuthCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "configure",
RunE: func(cmd *cobra.Command, args []string) error {
var key string
var err error
err = survey.AskOne(&survey.Password{
Message: "Enter the API key",
}, &key)
fmt.Print("Enter API Key: ")
key, err := term.ReadPassword(syscall.Stdin)
if err != nil {
logger.Fatalf("Failed to setup auth: %v", err)
panic(err)
}
if auth.TenantDomain() != "" && auth.TenantDomain() != authTenantDomain {
ui.PrintWarning("Tenant domain mismatch. Existing: %s, New: %s, continue? ",
auth.TenantDomain(), authTenantDomain)
err = auth.Configure(auth.Config{
ApiUrl: authInsightApiBaseUrl,
ApiKey: string(key),
})
var confirm bool
err = survey.AskOne(&survey.Confirm{
Message: "Do you want to continue?",
}, &confirm)
if err != nil {
logger.Fatalf("Failed to setup auth: %v", err)
}
if !confirm {
return nil
}
}
auth.SetRuntimeCloudTenant(authTenantDomain)
auth.SetRuntimeApiKey(key)
err = auth.Verify()
if err != nil {
logger.Fatalf("Failed to verify auth: %v", err)
panic(err)
}
err = auth.PersistApiKey(key, authTenantDomain)
if err != nil {
logger.Fatalf("Failed to configure auth: %v", err)
}
os.Exit(0)
os.Exit(1)
return nil
},
}
cmd.Flags().StringVarP(&authTenantDomain, "tenant", "", "",
"Tenant domain for SafeDep Cloud")
_ = cmd.MarkFlagRequired("tenant")
cmd.Flags().StringVarP(&authInsightApiBaseUrl, "api", "", auth.DefaultApiUrl(),
"Base URL of Insights API")
return cmd
}
func verifyAuthCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "verify",
RunE: func(cmd *cobra.Command, args []string) error {
if auth.CommunityMode() {
ui.PrintSuccess("Running in Community Mode")
}
command.FailOnError("auth/verify", auth.Verify())
ui.PrintSuccess("Authentication key is valid!")
fmt.Printf("Verify auth command is currently work in progress\n")
os.Exit(1)
return nil
},
}
return cmd
}
func trialsRegisterCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "trial",
RunE: func(cmd *cobra.Command, args []string) error {
client := auth.NewTrialRegistrationClient(auth.TrialConfig{
Email: authTrialEmail,
ControlPlaneApiUrl: authControlPlaneApiBaseUrl,
})
res, err := client.Execute()
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Trial registration successful with Id:%s\n", res.Id)
fmt.Printf("Check your email (%s) for API key and usage instructions\n", authTrialEmail)
fmt.Printf("The trial API key will expire on %s\n", res.ExpiresAt.String())
return nil
},
}
cmd.Flags().StringVarP(&authTrialEmail, "email", "", "",
"Email address to use for sending trial API key")
cmd.Flags().StringVarP(&authControlPlaneApiBaseUrl, "control-plane", "",
auth.DefaultControlPlaneApiUrl(), "Base URL of Control Plane API for registrations")
return cmd
}

View File

@ -1,53 +0,0 @@
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
}

View File

@ -1,34 +0,0 @@
// 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
}

View File

@ -1,107 +0,0 @@
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
}

View File

@ -1,57 +0,0 @@
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

View File

@ -1,191 +0,0 @@
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"
)
var (
keyName string
keyDescription string
keyExpiresIn int
listKeysName string
listKeysIncludeExpired bool
listKeysOnlyMine bool
deleteKeyId string
)
func newKeyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "key",
Short: "Manage API keys",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.AddCommand(newKeyCreateCommand())
cmd.AddCommand(newListKeyCommand())
cmd.AddCommand(newDeleteKeyCommand())
return cmd
}
func newDeleteKeyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "Delete an API key",
RunE: func(cmd *cobra.Command, args []string) error {
err := executeDeleteKey()
if err != nil {
logger.Errorf("Failed to delete API key: %v", err)
}
return nil
},
}
cmd.Flags().StringVar(&deleteKeyId, "id", "", "ID of the API key to delete")
_ = cmd.MarkFlagRequired("id")
return cmd
}
func executeDeleteKey() error {
client, err := auth.ControlPlaneClientConnection("vet-cloud-key-delete")
if err != nil {
return err
}
keyService, err := cloud.NewApiKeyService(client)
if err != nil {
return err
}
err = keyService.DeleteKey(deleteKeyId)
if err != nil {
return err
}
ui.PrintSuccess("API key deleted successfully.")
return nil
}
func newListKeyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List API keys",
RunE: func(cmd *cobra.Command, args []string) error {
err := executeListKeys()
if err != nil {
logger.Errorf("Failed to list API keys: %v", err)
}
return nil
},
}
cmd.Flags().StringVar(&listKeysName, "name", "",
"List keys with partial match on the name")
cmd.Flags().BoolVar(&listKeysIncludeExpired, "include-expired", false,
"Include expired keys in the list")
cmd.Flags().BoolVar(&listKeysOnlyMine, "only-mine", false,
"List only keys created by the current user")
return cmd
}
func executeListKeys() error {
client, err := auth.ControlPlaneClientConnection("vet-cloud-key-list")
if err != nil {
return err
}
keyService, err := cloud.NewApiKeyService(client)
if err != nil {
return err
}
keys, err := keyService.ListKeys(&cloud.ListApiKeyRequest{
Name: listKeysName,
IncludeExpired: listKeysIncludeExpired,
OnlyMine: listKeysOnlyMine,
})
if err != nil {
return err
}
if len(keys.Keys) == 0 {
ui.PrintSuccess("No API keys found.")
return nil
}
tbl := ui.NewTabler(ui.TablerConfig{})
tbl.AddHeader("ID", "Name", "Expires At", "Description")
for _, key := range keys.Keys {
expiresAt := key.ExpiresAt.In(time.Local).Format(time.RFC822)
tbl.AddRow(key.ID, key.Name, expiresAt, key.Desc)
}
return tbl.Finish()
}
func newKeyCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create a new API key",
RunE: func(cmd *cobra.Command, args []string) error {
err := executeCreateKey()
if err != nil {
logger.Errorf("Failed to create API key: %v", err)
}
return nil
},
}
cmd.Flags().StringVar(&keyName, "name", "", "Name of the API key")
cmd.Flags().StringVar(&keyDescription, "description", "", "Description of the API key")
cmd.Flags().IntVar(&keyExpiresIn, "expires-in", 30,
"Number of days after which the API key will expire")
_ = cmd.MarkFlagRequired("name")
return cmd
}
func executeCreateKey() error {
client, err := auth.ControlPlaneClientConnection("vet-cloud-key-create")
if err != nil {
return err
}
keyService, err := cloud.NewApiKeyService(client)
if err != nil {
return err
}
key, err := keyService.CreateApiKey(&cloud.CreateApiKeyRequest{
Name: keyName,
Desc: keyDescription,
ExpiryInDays: keyExpiresIn,
})
if err != nil {
return err
}
ui.PrintSuccess("API key created successfully.")
ui.PrintSuccess("Key: %s", key.Key)
ui.PrintSuccess("Expires at: %s", key.ExpiresAt.Format(time.RFC3339))
return nil
}

View File

@ -1,68 +0,0 @@
package cloud
import (
"context"
"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"
)
func newCloudLoginCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "login",
Short: "Login to SafeDep cloud for management tasks",
RunE: func(cmd *cobra.Command, args []string) error {
err := executeCloudLogin()
if err != nil {
logger.Errorf("Failed to login to the SafeDep cloud: %v", err)
}
return nil
},
}
return cmd
}
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 nil, fmt.Errorf("failed to request device code: %w", err)
}
ui.PrintSuccess("Please visit %s and enter the code %s to authenticate",
code.VerificationURIComplete, code.UserCode)
token, err := device.Wait(context.TODO(),
http.DefaultClient, auth.CloudIdentityServiceTokenUrl(),
device.WaitOptions{
ClientID: auth.CloudIdentityServiceClientId(),
DeviceCode: code,
})
if err != nil {
return nil, fmt.Errorf("failed to authenticate: %w", err)
}
return token, nil
}

View File

@ -1,87 +0,0 @@
package cloud
import (
"fmt"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
)
var (
tenantDomain string
outputCSV string
outputMarkdown string
)
func NewCloudCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cloud",
Short: "Manage and query cloud resources (control plane)",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.PersistentFlags().StringVar(&tenantDomain, "tenant", "",
"Tenant domain to use for the command")
cmd.PersistentFlags().StringVar(&outputCSV, "csv", "",
"Output table views to a CSV file")
cmd.PersistentFlags().StringVar(&outputMarkdown, "markdown", "",
"Output table views to a Markdown file")
cmd.AddCommand(newCloudLoginCommand())
cmd.AddCommand(newRegisterCommand())
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 != "" {
auth.SetRuntimeCloudTenant(tenantDomain)
}
}
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
}

View File

@ -1,50 +0,0 @@
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"
)
func newPingCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "ping",
Short: "Ping the control plane to check authentication and connectivity",
RunE: func(cmd *cobra.Command, args []string) error {
err := pingControlPlane()
if err != nil {
logger.Errorf("Failed to ping control plane: %v", err)
}
return nil
},
}
return cmd
}
func pingControlPlane() error {
conn, err := auth.ControlPlaneClientConnection("vet-cloud-ping")
if err != nil {
return err
}
pingService, err := cloud.NewPingService(conn)
if err != nil {
return err
}
pr, err := pingService.Ping()
if err != nil {
return err
}
ui.PrintSuccess("Ping successful. Started at %s, finished at %s",
pr.StartedAt.Format(time.RFC3339), pr.FinishedAt.Format(time.RFC3339))
return nil
}

View File

@ -1,178 +0,0 @@
package cloud
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"
)
var (
querySql string
queryPageSize int
)
func newQueryCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "query",
Short: "Query risks by executing SQL queries",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.AddCommand(newQuerySchemaCommand())
cmd.AddCommand(newQueryExecuteCommand())
return cmd
}
func newQuerySchemaCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "schema",
Short: "Get the schema for the query service",
RunE: func(cmd *cobra.Command, args []string) error {
err := getQuerySchema()
if err != nil {
logger.Errorf("Failed to get query schema: %v", err)
}
return nil
},
}
return cmd
}
func newQueryExecuteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "execute",
Short: "Execute a query",
RunE: func(cmd *cobra.Command, args []string) error {
err := executeQuery()
if err != nil {
logger.Errorf("Failed to execute query: %v", err)
}
return nil
},
}
cmd.Flags().StringVarP(&querySql, "sql", "s", "", "SQL query to execute")
cmd.Flags().IntVarP(&queryPageSize, "limit", "", 100, "Limit the number of results returned")
return cmd
}
func getQuerySchema() error {
client, err := auth.ControlPlaneClientConnection("vet-cloud-query")
if err != nil {
return err
}
queryService, err := query.NewQueryService(client)
if err != nil {
return err
}
response, err := queryService.GetSchema()
if err != nil {
return err
}
tbl := ui.NewTabler(ui.TablerConfig{
CsvPath: outputCSV,
MarkdownPath: outputMarkdown,
})
tbl.AddHeader("Name", "Column Name", "Selectable", "Filterable", "Reference")
schemas := response.GetSchemas()
for _, schema := range schemas {
schemaName := schema.GetName()
columns := schema.GetColumns()
sort.Slice(columns, func(i, j int) bool {
return columns[i].GetName() < columns[j].GetName()
})
for _, column := range columns {
tbl.AddRow(schemaName,
column.GetName(),
column.GetSelectable(),
column.GetFilterable(),
column.GetReferenceUrl())
}
}
return tbl.Finish()
}
func executeQuery() error {
if querySql == "" {
return errors.New("SQL string is required")
}
client, err := auth.ControlPlaneClientConnection("vet-cloud-query")
if err != nil {
return err
}
queryService, err := query.NewQueryService(client)
if err != nil {
return err
}
response, err := queryService.ExecuteSql(querySql, queryPageSize)
if err != nil {
return err
}
return renderQueryResponseAsTable(response)
}
func renderQueryResponseAsTable(response *query.QueryResponse) error {
tbl := ui.NewTabler(ui.TablerConfig{
CsvPath: outputCSV,
MarkdownPath: outputMarkdown,
})
if response.Count() == 0 {
logger.Infof("No results found")
return nil
}
ui.PrintSuccess("Query returned %d results", response.Count())
// Header
headers := []string{}
response.GetRow(0).ForEachField(func(key string, _ interface{}) {
headers = append(headers, key)
})
sort.Strings(headers)
headerRow := []interface{}{}
for _, header := range headers {
headerRow = append(headerRow, header)
}
tbl.AddHeader(headerRow...)
// Ensure we have a consistent order of columns
response.ForEachRow(func(row *query.QueryRow) {
rowValues := []interface{}{}
for _, header := range headers {
rowValues = append(rowValues, row.GetField(header))
}
tbl.AddRow(rowValues...)
})
return tbl.Finish()
}

View File

@ -1,314 +0,0 @@
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
}

View File

@ -1,77 +0,0 @@
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"
)
var (
registerEmail string
registerName string
registerOrgName string
registerOrgDomain string
)
func newRegisterCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "register",
Short: "Register a new user and tenant",
RunE: func(cmd *cobra.Command, args []string) error {
err := registerUserTenant()
if err != nil {
logger.Errorf("Failed to register user: %v", err)
}
return nil
},
}
cmd.Flags().StringVar(&registerEmail, "email", "cloud@safedep.io", "Email of the user (not required for SafeDep cloud)")
cmd.Flags().StringVar(&registerName, "name", "", "Name of the user")
cmd.Flags().StringVar(&registerOrgName, "org-name", "", "Name of the organization")
cmd.Flags().StringVar(&registerOrgDomain, "org-domain", "", "Domain of the organization")
_ = cmd.MarkFlagRequired("name")
_ = cmd.MarkFlagRequired("org-name")
_ = cmd.MarkFlagRequired("org-domain")
return cmd
}
func registerUserTenant() error {
conn, err := auth.ControlPlaneClientConnection("vet-cloud-register")
if err != nil {
return err
}
onboardingService, err := cloud.NewOnboardingService(conn)
if err != nil {
return err
}
res, err := onboardingService.Register(&cloud.RegisterRequest{
Name: registerName,
Email: registerEmail,
OrgName: registerOrgName,
OrgDomain: registerOrgDomain,
})
if err != nil {
return err
}
ui.PrintSuccess("Registered user and tenant.")
ui.PrintSuccess("Tenant domain: %s", res.TenantDomain)
err = auth.PersistTenantDomain(res.TenantDomain)
if err != nil {
return fmt.Errorf("failed to persist tenant domain: %w", err)
}
return nil
}

View File

@ -1,63 +0,0 @@
package cloud
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"
)
func newWhoamiCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "whoami",
Short: "Print information about the current user",
RunE: func(cmd *cobra.Command, args []string) error {
err := executeWhoami()
if err != nil {
logger.Errorf("Failed to execute whoami: %v", err)
}
return nil
},
}
return cmd
}
func executeWhoami() error {
conn, err := auth.ControlPlaneClientConnection("vet-cloud-whoami")
if err != nil {
return err
}
userService, err := cloud.NewUserService(conn)
if err != nil {
return err
}
res, err := userService.CurrentUserInfo()
if err != nil {
return err
}
tbl := ui.NewTabler(ui.TablerConfig{})
tbl.AddHeader("Email", "Tenant", "Access Level")
for _, access := range res.GetAccess() {
accessName := "UNSPECIFIED"
if name, ok := controltowerv1.AccessLevel_name[int32(access.GetLevel())]; ok {
accessName = name
}
tbl.AddRow(res.GetUser().GetEmail(),
access.GetTenant().GetDomain(),
fmt.Sprintf("%s (%d)", accessName, access.GetRole()))
}
return tbl.Finish()
}

View File

@ -1,33 +0,0 @@
package code
import (
"github.com/safedep/code/core"
"github.com/safedep/code/lang"
"github.com/safedep/vet/pkg/common/logger"
)
func getAllLanguageCodeStrings() ([]string, error) {
langs, err := lang.AllLanguages()
if err != nil {
return nil, err
}
var languageCodes []string
for _, lang := range langs {
languageCodes = append(languageCodes, string(lang.Meta().Code))
}
return languageCodes, nil
}
func getLanguagesFromCodes(languageCodes []string) ([]core.Language, error) {
var languages []core.Language
for _, languageCode := range languageCodes {
language, err := lang.GetLanguage(languageCode)
if err != nil {
logger.Fatalf("failed to get language for code %s: %v", languageCode, err)
return nil, err
}
languages = append(languages, language)
}
return languages, nil
}

View File

@ -1,28 +0,0 @@
package code
import (
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/command"
)
var languageCodes []string
func NewCodeCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "code",
Short: "Analyze source code",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
defaultAllLanguageCodes, err := getAllLanguageCodeStrings()
command.FailOnError("setup-default-languages", err)
cmd.PersistentFlags().StringArrayVar(&languageCodes, "lang", defaultAllLanguageCodes, "Source code languages to analyze")
cmd.AddCommand(newScanCommand())
return cmd
}

View File

@ -1,96 +0,0 @@
package code
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"
)
var (
dbPath string
appDirs []string
importDirs []string
excludePatterns []string
skipDependencyUsagePlugin bool
)
func newScanCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "scan",
Short: "Scan source code",
RunE: func(cmd *cobra.Command, args []string) error {
startScan()
return nil
},
}
cmd.Flags().StringVar(&dbPath, "db", "", "Path to create the sqlite database")
cmd.Flags().StringArrayVar(&appDirs, "app", []string{"."}, "Directories to scan for application code files")
cmd.Flags().StringArrayVar(&importDirs, "import-dir", []string{}, "Directories to scan for import files")
cmd.Flags().StringArrayVarP(&excludePatterns, "exclude", "", []string{},
"Name patterns to ignore while scanning a codebase")
cmd.Flags().BoolVar(&skipDependencyUsagePlugin, "skip-dependency-usage-plugin", false, "Skip dependency usage plugin analysis")
_ = cmd.MarkFlagRequired("db")
return cmd
}
func startScan() {
command.FailOnError("scan", internalStartScan())
}
func internalStartScan() error {
allowedLanguages, err := getLanguagesFromCodes(languageCodes)
if err != nil {
logger.Fatalf("failed to get languages from codes: %v", err)
return err
}
entSqliteStorage, err := storage.NewEntSqliteStorage(storage.EntSqliteClientConfig{
Path: dbPath,
ReadOnly: false,
SkipSchemaCreation: false,
})
if err != nil {
logger.Fatalf("failed to create ent sqlite storage: %v", err)
return err
}
excludePatternsRegexps := []*regexp.Regexp{}
for _, pattern := range excludePatterns {
excludePatternsRegexps = append(excludePatternsRegexps, regexp.MustCompile(pattern))
}
codeScanner, err := code.NewScanner(code.ScannerConfig{
AppDirectories: appDirs,
ImportDirectories: importDirs,
ExcludePatterns: excludePatternsRegexps,
Languages: allowedLanguages,
SkipDependencyUsagePlugin: skipDependencyUsagePlugin,
Callbacks: &code.ScannerCallbackRegistry{
OnScanStart: func() error {
ui.StartSpinner("Scanning code")
return nil
},
OnScanEnd: func() error {
ui.StopSpinner()
ui.PrintSuccess("🚀 Code scanning completed. Run vet scan with code context using --code flag")
return nil
},
},
}, entSqliteStorage)
if err != nil {
logger.Fatalf("failed to create code scanner: %v", err)
return err
}
return codeScanner.Scan(context.Background())
}

View File

@ -1,72 +0,0 @@
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
}

View File

@ -1,17 +0,0 @@
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
}

View File

@ -1,20 +0,0 @@
package inspect
import (
"github.com/spf13/cobra"
)
func NewPackageInspectCommand() *cobra.Command {
cmd := &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.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.AddCommand(newPackageMalwareInspectCommand())
return cmd
}

View File

@ -1,310 +0,0 @@
package inspect
import (
"context"
"fmt"
"os"
"strings"
"time"
"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/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/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 {
cmd := &cobra.Command{
Use: "malware",
Short: "Inspect an OSS package for malware",
Long: `Inspect an OSS package for malware using SafeDep Malware Analysis API`,
RunE: func(cmd *cobra.Command, args []string) error {
err := executeMalwareAnalysis()
if err != nil {
ui.PrintError("Failed: %v", err)
}
return nil
},
}
cmd.Flags().StringVar(&malwareAnalysisPackageUrl, "purl", "",
"Package URL to inspect for malware")
cmd.Flags().DurationVar(&malwareAnalysisTimeout, "timeout", 5*time.Minute,
"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")
return cmd
}
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
}
service := malysisv1grpc.NewMalwareAnalysisServiceClient(cc)
purl, err := pb.NewPurlPackageVersion(malwareAnalysisPackageUrl)
if err != nil {
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: packageVersion,
},
})
if err != nil {
return fmt.Errorf("failed to submit package for malware analysis: %v", err)
}
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)
}
if reportResponse.GetStatus() == malysisv1.AnalysisStatus_ANALYSIS_STATUS_FAILED {
return fmt.Errorf("malware analysis failed: %s", reportResponse.GetErrorMessage())
}
if reportResponse.GetStatus() == malysisv1.AnalysisStatus_ANALYSIS_STATUS_COMPLETED {
report = reportResponse.GetReport()
verificationRecord = reportResponse.GetVerificationRecord()
break
}
time.Sleep(5 * time.Second)
}
ui.StopSpinner()
if report == nil {
return fmt.Errorf("malware analysis report is empty")
}
ui.PrintSuccess("Malware analysis completed successfully")
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)
}
}
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), 0o644)
}
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()
tbl.SetOutputMirror(os.Stdout)
tbl.SetStyle(table.StyleLight)
tbl.AppendHeader(table.Row{"Package URL", "Status", "Confidence"})
status := reporter.InfoBgText(" SAFE ")
if report.GetInference().GetIsMalware() {
if vr != nil && vr.IsMalware {
status = reporter.CriticalBgText(" MALICIOUS ")
} else {
status = reporter.WarningBgText(" SUSPICIOUS ")
}
}
confidence := report.GetInference().GetConfidence().String()
confidence = strings.TrimPrefix(confidence, "CONFIDENCE_")
tbl.AppendRow(table.Row{purl, status, confidence})
tbl.Render()
fmt.Println()
fmt.Println(reporter.WarningText(fmt.Sprintf("** The full report is available at: %s",
reportVisualizationUrl(analysisId))))
fmt.Println()
return nil
}
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())
}

View File

@ -1,17 +0,0 @@
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
}

View File

@ -1,195 +0,0 @@
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(&registerVetSQLQueryTool, "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(&registerPackageRegistryTool, "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
}

View File

@ -1,60 +0,0 @@
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())
})
}

View File

@ -1,152 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"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"
)
func newConnectCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "connect",
Short: "Connect with 3rd party apps",
RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("a valid sub-command is required")
},
}
cmd.AddCommand(connectGithubCommand())
return cmd
}
func connectGithubCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "github",
RunE: func(cmd *cobra.Command, args []string) error {
githubAccessToken, err := getAccessTokenFromUser()
if err != nil {
githubAccessToken, err = getAccessTokenViaDeviceFlow()
}
if err != nil {
logger.Fatalf("Failed to connect with Github API: %s", err.Error())
}
err = connect.PersistGithubAccessToken(githubAccessToken)
if err != nil {
logger.Fatalf("Failed to persist Github connection token: %s", err.Error())
}
ui.PrintSuccess("Github Access Token configured and saved at '%s' for your convenience.", connect.GetConfigFileHint())
ui.PrintSuccess("You can use vet to scan your github repositories")
ui.PrintSuccess("Run the command to scan your github repository")
ui.PrintSuccess("\tvet scan --github https://github.com/<Org|User>/<Repo>")
os.Exit(1)
return nil
},
}
return cmd
}
func getAccessTokenFromUser() (string, error) {
var by_github_acces_token string
prompt := &survey.Select{
Message: "Do you have access token ready?",
Options: []string{"Y", "N"},
Default: "Y",
}
err := survey.AskOne(prompt, &by_github_acces_token)
if err != nil {
return "", err
}
if by_github_acces_token != "Y" {
return "", fmt.Errorf("user refused to provide access token")
}
password := &survey.Password{
Message: "Provide your access token: ",
}
var accessToken string
err = survey.AskOne(password, &accessToken)
if err != nil {
return "", err
}
return accessToken, nil
}
func getAccessTokenViaDeviceFlow() (string, error) {
var by_web_flow string
prompt := &survey.Select{
Message: "Do you want to connect with your Github account to continue?",
Options: []string{"Y", "N"},
Default: "Y",
}
err := survey.AskOne(prompt, &by_web_flow)
if err != nil {
return "", err
}
if by_web_flow != "Y" {
return "", fmt.Errorf("user cancelled device flow")
}
ui.PrintMsg("Starting Github authentication using oauth2 device flow")
token, err := connectGithubWithDeviceFlow()
if err != nil {
return "", err
}
return token, nil
}
func connectGithubWithDeviceFlow() (string, error) {
clientID := connect.GetGithubOAuth2ClientId()
scopes := []string{"repo", "read:org"}
httpClient := http.DefaultClient
logger.Debugf("Initiating Github device flow auth using clientId: %s", clientID)
// TODO: We are coupling with Github cloud API here. Self-hosted Github enterprise won't work
code, err := device.RequestCode(httpClient, "https://github.com/login/device/code", clientID, scopes)
if err != nil {
ui.PrintError("Error while requesting code from github: %s", err.Error())
return "", err
}
ui.PrintMsg("Copy the code: %s", code.UserCode)
ui.PrintMsg("Navigate to the URL and paste the code: %s", code.VerificationURI)
// TODO: We are coupling with Github cloud API here. Self-hosted Github enterprise won't work
accessToken, err := device.Wait(context.TODO(), httpClient,
"https://github.com/login/oauth/access_token",
device.WaitOptions{
ClientID: clientID,
DeviceCode: code,
})
if err != nil {
return "", err
}
logger.Debugf("Completed device flow with Github successfully")
return accessToken.Token, nil
}

20
docs/.gitignore vendored
View File

@ -1,20 +0,0 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -1 +0,0 @@
16.15.0

View File

@ -1,9 +0,0 @@
# vet Documentation
## Usage
`vet` user documentation is available at [https://docs.safedep.io/](https://docs.safedep.io/)
## Development
- [Storage](./storage.md)

View File

@ -1,40 +0,0 @@
# 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

View File

@ -1,35 +0,0 @@
# 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

115
docs/filtering.md Normal file
View File

@ -0,0 +1,115 @@
# 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.
## 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 |
## 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.
### Example Queries
| Description | Query |
|----------------------------------------------|---------------------------------------|
| Find packages with a critical vulnerability | `vulns.critical.exists(x, true)` |
| Find unmaintained packages as per OpenSSF SC | `scorecard.score["Maintenance"] == 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")`
Refer to [scorecard checks](https://github.com/ossf/scorecard#checks-1) for
a list of checks available from OpenSSF Scorecards project.
## Query Workflow
Scanning a package manifest is a resource intensive process as it involves
enriching package metadata by queryin [Insights API](https://safedep.io/docs/concepts/raya-data-platform-overview).
However, for 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-console
vet query --from /tmp/dump --filter 'scorecard.score["Maintenance"] == 0'
```
## FAQ
### 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"
]
}
```

View File

@ -1,37 +0,0 @@
title: SafeDep/vet Manual
description: CLI reference manual for SafeDep/vet. Next-gen software composition analysis and malicious package protection tool.
remote_theme: just-the-docs/just-the-docs@v0.8.0
markdown: kramdown
# Custom Color Schema, defined in _sass/color_schemes/safedep.scss
color_scheme: safedep
# GitHub repo link (appears in top right)
repo_url: https://github.com/safedep/vet
# Aux links (top navigation bar)
aux_links:
"GitHub": https://github.com/safedep/vet
aux_links_new_tab: true
# Heading anchor links (h1, h2, h3...)
heading_anchors: true
# Search functionality
search_enabled: true
search.heading_level: 2
# Logo (if you have one)
logo: "/assets/logo.png"
favicon_ico: "/assets/favicon.png"
# Footer
footer_content: "Copyright &copy; 2025 SafeDep Inc."
# Back to top button
back_to_top: true
back_to_top_text: "Back to top"
plugins:
- jekyll-seo-tag
- jekyll-github-metadata
- jekyll-include-cache
- jekyll-sitemap

View File

@ -1,19 +0,0 @@
// Change Default Theme (purple's variable) to branding color
// SafeDep Brand Color is #0d9488
$safedep-brand: #0d9488;
// This will override the the default color schema
$purple-000: #0c8d75; // A little bit dark in that color for links (much better for links)
$purple-100: #0a7562; // A little more dark on that link color for hover effect
$link-color: $purple-000; // Set Link Color
// Override variables for the .btn-primary button
$btn-primary-color: $safedep-brand;
$btn-primary-bg: $safedep-brand; // Background color of the button
$btn-primary-border: $safedep-brand; // Border color of the button
$btn-hover-color: #10bcad;
$btn-primary-hover-bg: $btn-hover-color; // Background color on hover
$btn-primary-hover-border: $btn-hover-color; // Border color on hover

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

View File

@ -1,28 +0,0 @@
---
title: SafeDep/vet Manual
layout: home
nav_order: 1
---
# SafeDep `vet` CLI manual
[`vet`](https://github.com/safedep/vet) is a free and open source software supply chain security tool. It helps developers and security engineers protect against malicious open source packages and establish policy driven guardrails.
<br />
> _This CLI reference provides detailed documentation for all vet commands, flags, and options._
[Go to CLI Manual](vet.html){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 }
## `vet` Installation
```bash
brew install safedep/tap/vet
```
See [other installation options](https://github.com/safedep/vet?tab=readme-ov-file#-installation-options)
## Other Resources
Website: <https://safedep.io>
Docs: [https://docs.safedep.io](https://docs.safedep.io/introduction)

View File

@ -1,266 +0,0 @@
# vet MCP Server
[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=vet-mcp&config=eyJjb21tYW5kIjoiZG9ja2VyIHJ1biAtLXJtIC1pIGdoY3IuaW8vc2FmZWRlcC92ZXQ6bGF0ZXN0IC1zIC1sIC90bXAvdmV0LW1jcC5sb2cgc2VydmVyIG1jcCJ9)
The `vet` MCP server is designed to run locally using `stdio` or `sse` transports.
It provides tools for MCP clients such as Claude Code, Cursor and others to vet
open source packages before they are used in a project through AI generated code.
`vet` MCP server can protect against [Slopsquatting](https://en.wikipedia.org/wiki/Slopsquatting) attacks, malicious packages,
vulnerabilities and other security risks.
## Supported Ecosystems
`vet` MCP server currently supports the following ecosystems:
- npm
- PyPI
## Usage
Start the MCP server using SSE transport:
```bash
vet server mcp --server-type sse
```
Start the MCP server using stdio transport:
```bash
vet -s -l /tmp/vet-mcp.log server mcp --server-type stdio
```
> Avoid using `stdout` logging as it will interfere with the MCP server output.
### SSE Transport Features
The SSE (Server-Sent Events) transport supports:
- **GET requests**: For establishing SSE connections to receive real-time events
- **HEAD requests**: For endpoint health checks and capability probing (useful for tools like Langchain)
- **POST requests**: For sending messages to the MCP server via the message endpoint
The SSE endpoint returns appropriate headers for HEAD requests without a body, allowing tools to verify endpoint availability and capabilities.
### Security: Host and Origin Guards
For SSE, the server enforces simple, user-configurable guards to reduce the risk
of unauthorized cross-origin access and DNS rebinding attacks.
- **Host guard**: Only allows connections whose `Host` header matches an allowed
host list.
- **Origin guard**: For browser requests, only allows requests whose `Origin`
starts with an allowed prefix.
These checks are on by default with sensible localhost defaults, and you can
customize them with flags when starting the server.
#### Defaults
- **Allowed hosts**: `localhost:9988`, `127.0.0.1:9988`, `[::1]:9988`
- **Allowed origin prefixes**: `http://localhost:`, `http://127.0.0.1:`, `https://localhost:`
Requests that fail the host check are rejected with status `403`, and requests
that fail the origin check are rejected with status `403`.
#### Customize allowed hosts and origins
You can override the defaults using the following flags:
```bash
vet server mcp \
--server-type sse \
--sse-allowed-hosts "localhost:8080,127.0.0.1:8080" \
--sse-allowed-origins "http://localhost:,https://localhost:"
```
If you are running behind a proxy or using a different port, set both lists to
match your environment. For example, when exposing SSE on port 3001:
```bash
vet server mcp \
--server-type sse \
--sse-allowed-hosts "localhost:3001,127.0.0.1:3001" \
--sse-allowed-origins "http://localhost:,http://127.0.0.1:,https://localhost:"
```
With Docker, append the same flags to the container command:
```bash
docker run --rm -i ghcr.io/safedep/vet:latest \
server mcp \
--server-type sse \
--sse-allowed-hosts "localhost:9988,127.0.0.1:9988" \
--sse-allowed-origins "http://localhost:,http://127.0.0.1:,https://localhost:"
```
Tip: Non-browser clients may omit the `Origin` header. Those requests are
allowed as long as the host guard passes.
## Configure MCP Client
> **Note:** The example below uses pre-build docker image. You can build your own by running
> `docker build -t ghcr.io/safedep/vet:latest .`
Add following configuration to your MCP client configuration file:
```json
"vet-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"ghcr.io/safedep/vet:latest",
"-l",
"/tmp/vet-mcp.log",
"server",
"mcp"
]
}
```
`vet` uses `stdio` by default, if you wish to use SSE transport, add following args: `"--server-type", "sse"`
**Note:** While `docker` containers can help keep the MCP server process isolated, it needs to be periodically updated to the latest version.
Alternatively, you can use `vet` binary directly as an MCP server using `stdio` transport. Use the following command to start the MCP server:
```bash
vet -l /tmp/vet-mcp.log server mcp --server-type stdio
```
### Configure IDE
#### Cursor
Add `vet-mcp` server to either `.cursor/mcp.json` (project specific configuration) or `~/.cursor/mcp.json` (applies to all projects)
```json
{
"mcpServers": {
"vet-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"ghcr.io/safedep/vet:latest",
"-l",
"/tmp/vet-mcp.log",
"server",
"mcp"
]
}
}
}
```
Add the following to `.cursor/rules/vet-mcp.mdc` file:
```
---
description: vet Open Source Packages using vet MCP server
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
```
Example usage with Cursor:
![Cursor vet-mcp example](./assets/vet-mcp-cursor.png)
[📄 Cursor MCP server reference](https://docs.cursor.com/context/model-context-protocol#manual-configuration)
#### Visual Studio Code
Add `vet-mcp` server to `.vscode/mcp.json` (project specific configuration)
```json
{
"servers": {
"vet-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"ghcr.io/safedep/vet:latest",
"-l",
"/tmp/vet-mcp.log",
"server",
"mcp"
]
}
}
}
```
In order to use `vet-mcp` for all projects in Visual Studio Code, add following `mcp` setting in [Visual Studio Code User Settings](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server-to-your-user-settings) (`settings.json`)
```json
{
"mcp": {
"servers": {
"vet-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"ghcr.io/safedep/vet:latest",
"-l",
"/tmp/vet-mcp.log",
"server",
"mcp"
]
}
}
}
}
```
Add the following to `.github/copilot-instructions.md` file:
```
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
```
Example usage with Visual Studio Code:
![Visual Studio Code vet-mcp example](./assets/vet-mcp-vscode.png)
[📄 Visual Studio Code MCP server reference](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
#### Claude Code
Add the following to `.mcp.json` in your Claude Code project:
```json
{
"mcpServers": {
"vet-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"ghcr.io/safedep/vet:latest",
"server",
"mcp"
]
}
}
}
```
**Note:** You can also use `vet` binary directly as an MCP server using `stdio` transport.

View File

@ -1,21 +0,0 @@
## OSV (OSSF) Report
Using `--report-osv` we can generate report for `OSSF` malicious package database.
Usage:
```bash
vet inspect malware --purl ... --report-osv .
```
The value of `--report-osv` is the root of [ossf/malicious-packages](https://github.com/ossf/malicious-packages/) repository,
it automatically places the JSON report in correct location, like `osv/malicious/npm/...`.
Flags:
| Flag | Usage | Default Value |
| -------------------------- | ----------------------------------- | --------------------------------------------- |
| `report-osv-finder-name` | Name of finder | `SafeDep` |
| `report-osv-contacts` | Contact Info, email, website etc | `https://safedep.io` |
| `report-osv-reference-url` | Report Reference URL, like blog etc | `https://app.safedep.io/community/malysis/ID` |
| `report-osv-with-ranges` | Use `ranges` affected property | discrete `versions` |

View File

@ -1,47 +0,0 @@
# Policy Engine Development
The policy engine is implemented using [Common Expressions Languages](https://cel.dev).
This development document is ONLY for Policy v2, internally represented
as Filter V2 for naming consistency.
## Enum Constants
Protobuf enums are exposed as integer values in CEL. To improve policy readability, we generate enum constant maps that allow using symbolic names instead of integers.
**Example usage in policies:**
```cel
// Instead of: p.project.type == 1
p.project.type == ProjectSourceType.GITHUB
// Instead of: pkg.ecosystem == 2
pkg.ecosystem == Ecosystem.NPM
```
**How it works:**
- `pkg/analyzer/filterv2/enums.go` registers enums via `RegisteredEnums` by referencing protobuf-generated `Type_value` maps
- `pkg/analyzer/filterv2/enumgen/` generates `enums_generated.go` with constant maps
- Run `go generate ./pkg/analyzer/filterv2/` to regenerate after adding new enums
**Adding new enums:**
1. Add entry to `RegisteredEnums` in `pkg/analyzer/filterv2/enums.go`:
```go
{
Name: "SeverityRisk",
Prefix: "RISK_",
ValueMap: vulnerabilityv1.Severity_Risk_value,
}
```
2. Declare the enum variable in `pkg/analyzer/filterv2/eval.go` `NewEvaluator()`:
```go
cel.Variable("SeverityRisk", cel.MapType(cel.StringType, cel.IntType))
```
3. Run `go generate ./pkg/analyzer/filterv2/`
The generator automatically strips prefixes (e.g., `RISK_CRITICAL``CRITICAL`) and keeps enums synchronized with protobuf definitions.

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
# Storage
`vet` contains a storage engine defined in `pkg/storage`. We use `sqlite3` as
the database and [ent](https://entgo.io/) as the ORM.
## Usage
- Create new schema using the following command
```shell
go run -mod=mod entgo.io/ent/cmd/ent new CodeSourceFile
```
- Schemas are generated in `./ent/schema` directory
- Edit the generated schema file and add the necessary fields and edges
- Generate the models from the schema using the following command
```shell
make ent
```
- Make sure to commit any changes to `ent` directory including the generated
files
## Guidance
All schemas are stored in `./ent/schema` directory. To avoid naming conflicts,
prefer prefixing the schema name with the logical module name. Example: `CodeSourceFile` is
used as the schema for storing `SourceFile` within `Code` analysis module.

File diff suppressed because it is too large Load Diff

View File

@ -1,129 +0,0 @@
// Code generated by ent, DO NOT EDIT.
package ent
import (
"fmt"
"strings"
"entgo.io/ent"
"entgo.io/ent/dialect/sql"
"github.com/safedep/vet/ent/codesourcefile"
)
// CodeSourceFile is the model entity for the CodeSourceFile schema.
type CodeSourceFile struct {
config `json:"-"`
// ID of the ent.
ID int `json:"id,omitempty"`
// Path holds the value of the "path" field.
Path string `json:"path,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the CodeSourceFileQuery when eager-loading is set.
Edges CodeSourceFileEdges `json:"edges"`
selectValues sql.SelectValues
}
// CodeSourceFileEdges holds the relations/edges for other nodes in the graph.
type CodeSourceFileEdges struct {
// DepsUsageEvidences holds the value of the deps_usage_evidences edge.
DepsUsageEvidences []*DepsUsageEvidence `json:"deps_usage_evidences,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
loadedTypes [1]bool
}
// DepsUsageEvidencesOrErr returns the DepsUsageEvidences value or an error if the edge
// was not loaded in eager-loading.
func (e CodeSourceFileEdges) DepsUsageEvidencesOrErr() ([]*DepsUsageEvidence, error) {
if e.loadedTypes[0] {
return e.DepsUsageEvidences, nil
}
return nil, &NotLoadedError{edge: "deps_usage_evidences"}
}
// scanValues returns the types for scanning values from sql.Rows.
func (*CodeSourceFile) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case codesourcefile.FieldID:
values[i] = new(sql.NullInt64)
case codesourcefile.FieldPath:
values[i] = new(sql.NullString)
default:
values[i] = new(sql.UnknownType)
}
}
return values, nil
}
// assignValues assigns the values that were returned from sql.Rows (after scanning)
// to the CodeSourceFile fields.
func (csf *CodeSourceFile) assignValues(columns []string, values []any) error {
if m, n := len(values), len(columns); m < n {
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
}
for i := range columns {
switch columns[i] {
case codesourcefile.FieldID:
value, ok := values[i].(*sql.NullInt64)
if !ok {
return fmt.Errorf("unexpected type %T for field id", value)
}
csf.ID = int(value.Int64)
case codesourcefile.FieldPath:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field path", values[i])
} else if value.Valid {
csf.Path = value.String
}
default:
csf.selectValues.Set(columns[i], values[i])
}
}
return nil
}
// Value returns the ent.Value that was dynamically selected and assigned to the CodeSourceFile.
// This includes values selected through modifiers, order, etc.
func (csf *CodeSourceFile) Value(name string) (ent.Value, error) {
return csf.selectValues.Get(name)
}
// QueryDepsUsageEvidences queries the "deps_usage_evidences" edge of the CodeSourceFile entity.
func (csf *CodeSourceFile) QueryDepsUsageEvidences() *DepsUsageEvidenceQuery {
return NewCodeSourceFileClient(csf.config).QueryDepsUsageEvidences(csf)
}
// Update returns a builder for updating this CodeSourceFile.
// Note that you need to call CodeSourceFile.Unwrap() before calling this method if this CodeSourceFile
// was returned from a transaction, and the transaction was committed or rolled back.
func (csf *CodeSourceFile) Update() *CodeSourceFileUpdateOne {
return NewCodeSourceFileClient(csf.config).UpdateOne(csf)
}
// Unwrap unwraps the CodeSourceFile entity that was returned from a transaction after it was closed,
// so that all future queries will be executed through the driver which created the transaction.
func (csf *CodeSourceFile) Unwrap() *CodeSourceFile {
_tx, ok := csf.config.driver.(*txDriver)
if !ok {
panic("ent: CodeSourceFile is not a transactional entity")
}
csf.config.driver = _tx.drv
return csf
}
// String implements the fmt.Stringer.
func (csf *CodeSourceFile) String() string {
var builder strings.Builder
builder.WriteString("CodeSourceFile(")
builder.WriteString(fmt.Sprintf("id=%v, ", csf.ID))
builder.WriteString("path=")
builder.WriteString(csf.Path)
builder.WriteByte(')')
return builder.String()
}
// CodeSourceFiles is a parsable slice of CodeSourceFile.
type CodeSourceFiles []*CodeSourceFile

View File

@ -1,83 +0,0 @@
// Code generated by ent, DO NOT EDIT.
package codesourcefile
import (
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
)
const (
// Label holds the string label denoting the codesourcefile type in the database.
Label = "code_source_file"
// FieldID holds the string denoting the id field in the database.
FieldID = "id"
// FieldPath holds the string denoting the path field in the database.
FieldPath = "path"
// EdgeDepsUsageEvidences holds the string denoting the deps_usage_evidences edge name in mutations.
EdgeDepsUsageEvidences = "deps_usage_evidences"
// Table holds the table name of the codesourcefile in the database.
Table = "code_source_files"
// DepsUsageEvidencesTable is the table that holds the deps_usage_evidences relation/edge.
DepsUsageEvidencesTable = "deps_usage_evidences"
// DepsUsageEvidencesInverseTable is the table name for the DepsUsageEvidence entity.
// It exists in this package in order to avoid circular dependency with the "depsusageevidence" package.
DepsUsageEvidencesInverseTable = "deps_usage_evidences"
// DepsUsageEvidencesColumn is the table column denoting the deps_usage_evidences relation/edge.
DepsUsageEvidencesColumn = "deps_usage_evidence_used_in"
)
// Columns holds all SQL columns for codesourcefile fields.
var Columns = []string{
FieldID,
FieldPath,
}
// ValidColumn reports if the column name is valid (part of the table columns).
func ValidColumn(column string) bool {
for i := range Columns {
if column == Columns[i] {
return true
}
}
return false
}
var (
// PathValidator is a validator for the "path" field. It is called by the builders before save.
PathValidator func(string) error
)
// OrderOption defines the ordering options for the CodeSourceFile queries.
type OrderOption func(*sql.Selector)
// ByID orders the results by the id field.
func ByID(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldID, opts...).ToFunc()
}
// ByPath orders the results by the path field.
func ByPath(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldPath, opts...).ToFunc()
}
// ByDepsUsageEvidencesCount orders the results by deps_usage_evidences count.
func ByDepsUsageEvidencesCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborsCount(s, newDepsUsageEvidencesStep(), opts...)
}
}
// ByDepsUsageEvidences orders the results by deps_usage_evidences terms.
func ByDepsUsageEvidences(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newDepsUsageEvidencesStep(), append([]sql.OrderTerm{term}, terms...)...)
}
}
func newDepsUsageEvidencesStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(DepsUsageEvidencesInverseTable, FieldID),
sqlgraph.Edge(sqlgraph.O2M, true, DepsUsageEvidencesTable, DepsUsageEvidencesColumn),
)
}

Some files were not shown because too many files have changed in this diff Show More