Compare commits

...

205 Commits
v1.9.2 ... main

Author SHA1 Message Date
Sahil Bansal
8594b205ee
add support for linux/arm64 (#652) 2025-12-10 16:16:20 +05:30
Abhisek Datta
6acf08aec0
feat: Exclude Fork and Archive during GitHub Org Scan (#650)
* Only scan private repos under org based scan

* Only scan private repos under org based scan

* fix: Style and formatting issues

* Update scan.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>

* fix: Add tests for github org reader filters

---------

Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>
Co-authored-by: infosecwonderland <monika.talekar@ascenda.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-09 16:44:49 +00:00
Sahil Bansal
33c4ca5059
Fix CycloneDX SBOM validation (#647)
* fix component duplication & license format

* fix tests

* use bool type for consistency
2025-12-04 09:34:59 +05:30
Sahil Bansal
a430641960
Add bun.lock parser support (#636)
* add bun.lock file parser support

* fix linter and update ListParsers test case

* fix linter
2025-12-01 12:56:48 +05:30
Kunal Singh
76788aa1cb
fix: json matching for mcp publish verify (#645) 2025-12-01 10:46:35 +05:30
Kunal Singh
dbc2eadac3
fix: verify mcp publish job failing (#642)
* fix: verify mcp publish failing

* added comments

* fix typo
2025-11-27 11:21:38 +00:00
Kunal Singh
ecf8c93f3f
Merge pull request #644 from safedep/chore/precommit-fmt-code
chore: Add golangci fmt for pre-commit
2025-11-27 15:11:34 +05:30
abhisek
070c5f5a24
chore: Add golangci fmt for pre-commit 2025-11-27 15:00:10 +05:30
Oleksandr Redko
4e39cebe61
chore: add formatters to golangci-lint config (#643)
Signed-off-by: Oleksandr Redko <oleksandr.red+github@gmail.com>
2025-11-27 14:58:24 +05:30
Kunal Singh
767e7cb16e
fix: OCI packages must not have 'version' field (#641) 2025-11-26 17:28:12 +05:30
Kunal Singh
4da939276e
fix: description validation error, mcp publishing (#640) 2025-11-26 16:32:38 +05:30
Kunal Singh
dc5846fb93
Merge pull request #639 from safedep/fix-mcp-ci-wd
fix: wrong working directory construction in mcp publishing CI
2025-11-26 14:44:35 +05:30
Kunal Singh
18a996dd53 relative path 2025-11-26 14:38:06 +05:30
Kunal Singh
333729a032 fix: path 2025-11-26 14:36:09 +05:30
Kunal Singh
0a67953b3f checkout before anyting 2025-11-26 14:33:02 +05:30
Kunal Singh
3316c81f35 fix: wrong working directory construction 2025-11-26 14:27:41 +05:30
Kunal Singh
51e185a09f
Merge pull request #638 from safedep/fix-mcp-ci
fix: working directory to .mcp-publisher
2025-11-26 14:08:34 +05:30
Kunal Singh
4599bacf17 fix: working directory to .mcp-publisher 2025-11-26 14:01:46 +05:30
Kunal Singh
e971466097
feat: vet-mcp registry publishing (#635)
* feat: ver-mcp registry publishing

* Update .github/workflows/publish-mcp.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* feat: verify job for mcp server publish

* feat: refactor server.json location

* fix: cd to .mcp-publisher dir

* update logo urls

* refactor: publish to mcp when container is build

---------

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 13:20:34 +05:30
Kunal Singh
65c44c97c1
Merge pull request #634 from safedep/vet-mcp-cursor-download-link-fix
Update cursor installation link for MCP Server
2025-11-24 13:25:39 +05:30
Kunal Singh
1c824c5515
Update cursor installation link for MCP Server
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-11-24 13:04:00 +05:30
Abhisek Datta
e1908e783b
chore: Update SafeDep Cloud URLs (#632)
* fix: SafeDep cloud URLs

* fix: Markdown formatting issue
2025-11-10 06:45:13 +00:00
Kunal Singh
6f5d0cea33
fix: github action versions not pinned and top level permissions missing (#631) 2025-10-25 16:45:58 +05:30
Sahil Bansal
dc3bc11a51
add git in vet image (#629) 2025-10-17 14:35:23 +05:30
Sahil Bansal
9aebb9d68e
fix: Handle Multiple License Type in npm Graph Parser (#625)
* do not fail on non string values for npm graph license field

* add non-string license type package for testing

* fix: Handle multiple cases for npm package-json license

---------

Co-authored-by: abhisek <abhisek.datta@gmail.com>
2025-10-15 09:30:21 +05:30
Abhisek Datta
801e81ac0d
fix: Use standard colors for inspect malware command (#624) 2025-10-13 04:20:06 +00:00
Kunal Singh
aa06dc6d93
feat: using branch color in manual (#623) 2025-10-13 09:06:01 +05:30
Kunal Singh
8e02db1d09
Merge pull request #622 from safedep/refactor/remove-local-manaul-gen
refactor: remove locally generated manual flow
2025-10-11 10:15:41 +05:30
Kunal Singh
6ad8539938 generate vet cli manual in ./docs/manual dir 2025-10-10 22:28:48 +05:30
Kunal Singh
6ade12a5f6 refactor: remove locally generated manual flow 2025-10-10 22:19:26 +05:30
Kunal Singh
521a5a5756
Merge pull request #621 from safedep/fix/gh-pages-ci
fix: github pages CI build jekyll and deploy to new service
2025-10-10 21:53:48 +05:30
Kunal Singh
54bade56e9 fix: build only after safe generation 2025-10-10 21:45:49 +05:30
Kunal Singh
1aecf640aa fix: typo 2025-10-10 21:43:13 +05:30
Kunal Singh
b1738ac35a fix: github pages CI build jekyll and deploy to new service 2025-10-10 21:40:36 +05:30
Kunal Singh
5663b59636
Merge pull request #620 from safedep/fix/gh-pages-ci-ignore-pattern-dir
fix: added ignore pattern for dynamic lines depending on current work…
2025-10-10 21:18:49 +05:30
Kunal Singh
dc14669605 fix: added ignore pattern for dynamic lines depending on current working directory 2025-10-10 21:08:34 +05:30
Kunal Singh
a8cd3a5b08
feat: github pages cli reference manual deploy (#619)
* feat: github pages cli reference manual deploy

* fix: typos

* feat: simplified logic, use gen folder

* fix: comments, typos, grammar

* update: manual home page

* fix: using generated files in ./docs/manual and not in subdir

* update: manual index (home) with vet.html link - more explicit

* refactor: copy changes
2025-10-10 18:56:31 +05:30
Kunal Singh
2d74224fd3
feat: added internal doc generate command (#618)
* feat: added internal doc generate command

* make generate and go mod tidy
2025-10-09 15:51:15 +05:30
Kunal Singh
b4aaf026c9
fix: recommended way os using golangci lint (#615)
* fix: recommended way os using golangci lint

* make generate

---------

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-10-06 15:31:02 +00:00
Kunal Singh
e48452dd05
feat: show manifest relative path in summary report (#613)
* feat: show manifest relative path in summary report

* fix: typos

* fix: using standard color profile

* fix: log message

---------

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-10-06 20:53:04 +05:30
Abhisek Datta
9f55120d2d
fix: Console Color Profiles (#610)
* fix: Color profile for console

* fix: Console color profile for dark mode

* fix: Simplify color profile identification using charm

* docs: Add trade-off comments
2025-10-06 10:22:48 +05:30
Kunal Singh
f88f76a1c5
Merge pull request #611 from safedep/container-e2e
feat: added vet scan container e2e
2025-10-04 13:21:29 +05:30
Kunal Singh
08e8915242 feat: added vet scan container e2e 2025-10-04 13:13:31 +05:30
Copilot
d5a64a4f61
Remove Markdown Builder in Favor of DRY Markdown Builder (#608)
* Initial plan

* Replace local markdown package with dry library version

- Updated dry dependency to v0.0.0-20250916040320-209e39edc57f (latest)
- Replaced imports from pkg/reporter/markdown to dry/reporting/markdown
- Removed local pkg/reporter/markdown directory
- All tests passing, build successful

Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>
Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
2025-10-02 23:09:48 +05:30
Copilot
8671c25fb5
Fix #606: Refactor NewEvaluator to use Option pattern and add defensive nil checks (#607)
* Initial plan

* Refactor NewEvaluator to use Option pattern and add defensive nil checks

- Implemented Option function type for configuring evaluator
- Added WithIgnoreError option function
- Updated NewEvaluator to accept only name parameter and variadic options
- Updated all usages in pkg/analyzer/filterv2/ and pkg/analyzer/ directories
- Added defensive nil checks for enum constants in EvaluatePackage
- Added comprehensive tests for Option pattern
- All tests pass successfully

Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>
2025-10-02 12:28:25 +05:30
Abhisek Datta
3eb501c72b
feat: Add Support for Filter v2 - Spec Based Policy Engine (#560)
* feat: Add support for filter v2

* Add filter v2 support

* Add test for filter v2 evaluator

* fix: CEL v2 query engine

* chore: Add sample policy-v2

* chore: Remove deprecated API use

* fix: Remove deprecated API

* fix: Misc linter fixes

* fix: Linter fixes

* fix: Policy v2 sample

* refactor: Extract filter match rendering into common concern

* test: Update test case

* fix: JSON round trip problem by using PB messages for filter eval

* Improve DX by declaring enum constants in CEL env

* fix: Code generator for enum type names

* docs: Add policy dev enumgen docs

* chore: Add example policy

* test: Add test for CEL filter suite v2 analyser

* Code review fixes

* fix: Code review comments

* chore: Add CI check to ensure generated code is updated

* fix: Add nil guards during init
2025-10-02 09:01:50 +05:30
Kunal Singh
498f809e94
Merge pull request #605 from safedep/test/add-test-cases-for-relative-exclusion-matcher
test: Add test case for relative exclusion matcher
2025-10-01 17:10:04 +05:30
abhisek
c2cb4375f8
test: Add test case for relative exclusion matcher 2025-10-01 13:51:58 +05:30
Sahil Bansal
7f8b335393
update docs (#604) 2025-09-25 15:48:56 +05:30
Sahil Bansal
969404956c
skips analytics init for root or help cmd/flags (#603)
* skips analytics init for root or help cmd/flags

* fix analytics init only on non-help cmds
2025-09-24 19:23:54 +05:30
Sahil Bansal
e9d3da03db
Fix Attest build provenance failing (#602)
* upload dist folder to make it accessible in provenance job

* pin commit sha's for uploading & downloading artifacts
2025-09-22 15:29:21 +05:30
Abhisek Datta
ec792952f8
fix/dir reader log path failure (#599)
* fix: Avoid hard failure in dir scanner on path errors

* fix: Ensure dir reader fails if root path doesn't exist
2025-09-21 15:59:45 +00:00
Copilot
d94a05844e
Add custom reference URL and version format control for OSV malware reports (#598)
* Initial plan

* Implement OSV report improvements with custom reference URLs and version format control

- Add --report-reference-url flag for custom reference URLs instead of default platform.safedep.io
- Add --range flag to control version representation:
  - With --range: use range-based versioning (SEMVER/ECOSYSTEM)
  - Without --range: use explicit versions array (new default)
- Update OSV report generation to support both modes
- Add comprehensive tests for new functionality
- Backward compatibility maintained for existing functionality

Co-authored-by: KunalSin9h <82411321+KunalSin9h@users.noreply.github.com>

* fix: custom reference url and added docs

* fix: docs table layout

* fix: test

* refactor: remove unnecessary else statement

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: KunalSin9h <82411321+KunalSin9h@users.noreply.github.com>
Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
2025-09-17 16:09:32 +05:30
Sahil Bansal
c449d94dad
pin building version to sha (#595) 2025-09-13 18:53:52 +05:30
Arunanshu Biswas
c0144e60e3
feat: update go version to latest (#589)
* feat: update go version to latest

* update workflow

* use bookworm

* revert fixture

* fix dockerfile run

* use more modern debian image

---------

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-09-13 10:25:25 +05:30
Kunal Singh
d3c87b6e06
fix: scalibr issue when runningSystem is true (#591) 2025-09-08 16:56:56 +05:30
Arunanshu Biswas
0ae3560ba1
fix: mcp SSE hardening (#587) 2025-09-04 06:37:59 +05:30
Kunal Singh
e4e9dc2590
Merge pull request #586 from safedep/fix/npm-publish-use-head-branch-tag
use head_branch for extracting tag
2025-08-27 18:21:55 +05:30
Sahilb315
f2a4722d04 use head_branch for extracting tag 2025-08-27 18:03:04 +05:30
Sahil Bansal
850f2c1dc9
fix syntax issue (#585) 2025-08-27 16:06:29 +05:30
Sahil Bansal
6dcbd15923
fix npm publish workflow to execute after goreleaser completion (#581)
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-08-27 08:06:00 +00:00
Kyle Kelly
82505f2460
Refactor goreleaser workflow to use attest-build-provenance (#584) 2025-08-27 07:16:50 +05:30
Kunal Singh
1afeb397f7
fix: extra space in ascii art (#582) 2025-08-26 09:10:45 +05:30
Sahil Bansal
5844d4ffd1
add support for generating sbom for homebrew installed packages (#571)
* add support for generating sbom for homebrew installed packages

* add brew test cases

* Update scan.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>

* minor error improvements

* add brew ecosystem

* maintain consistency in ecosystem name

* rename `brew` flag to `homebrew`

---------

Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-25 12:00:02 +05:30
Kunal Singh
651b09b085
Merge pull request #579 from safedep/readme-gitlab-fix
Change vet CI Component path in README
2025-08-23 17:45:56 +05:30
Kunal Singh
118210c7c7
Merge branch 'main' into readme-gitlab-fix 2025-08-23 17:40:28 +05:30
Copilot
269b843bb7
Improve MCP vulnerability API performance by using dedicated endpoint (#574)
* Initial plan

* Update dependencies and plan Get Vulnerabilities Tool implementation

Co-authored-by: arunanshub <48434243+arunanshub@users.noreply.github.com>

* Add GetPackageVersionVulnerabilitiesOnly driver method and comprehensive tests

Co-authored-by: arunanshub <48434243+arunanshub@users.noreply.github.com>

* Complete implementation by registering new vulnerability tool in MCP server

Co-authored-by: arunanshub <48434243+arunanshub@users.noreply.github.com>

* Address feedback: Remove separate vulnerability tool and use dedicated API in existing method

Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>

* Fix scanner mock interface after protobuf dependency update

Co-authored-by: arunanshub <48434243+arunanshub@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arunanshub <48434243+arunanshub@users.noreply.github.com>
Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-08-23 12:08:59 +00:00
Kunal Singh
68609f7397
Change vet CI Component path in README
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-08-23 17:37:01 +05:30
Kunal Singh
d0d21e6710
feat: updated vet in action demo gif (#578)
* feat: updated vet in action demo gif

* fix: replaced old gif

---------

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-08-23 12:03:09 +00:00
Kunal Singh
319cfdac5e
Added screenshot image to top in readme (#577)
* Added screenshort image to top in readme

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* added local file

---------

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-08-23 17:27:58 +05:30
Kunal Singh
458391e6f0
fix: progress bar overlaping other previous tables etc (#576) 2025-08-22 17:35:08 +05:30
Kunal Singh
c8ad28bf1a
Merge pull request #572 from safedep/new-vet-banner
feat: new vet ascii banner
2025-08-21 22:10:44 +05:30
Kunal Singh
3791b9555c fix: check for commit lenght 2025-08-21 21:53:19 +05:30
Kunal Singh
30a7b484d1 feat: new vet ascii banner 2025-08-21 21:51:58 +05:30
Copilot
41684afa80
Fix OSV schema for PyPI ecosystem: use proper case "PyPI" and ECOSYSTEM range type (#570)
* Initial plan

* Fix OSV schema for PyPI ecosystem - use proper case and ECOSYSTEM range type

Co-authored-by: KunalSin9h <82411321+KunalSin9h@users.noreply.github.com>

* Add clarifying comments and rename ecosystem mapping variables for better intention revealing

Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: KunalSin9h <82411321+KunalSin9h@users.noreply.github.com>
Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>
2025-08-21 19:08:38 +05:30
Sahil Bansal
2d06114eb7
add cloud session refresh using refresh token functionality (#565)
* add cloud session refresh using refresh token functionality

* add ui msg & update globalConfig to fallback to default when nil

* rm unnecessary comment

* refactor access token checks and error handling for cloud session refresh

* print error to user for automatic re-login

---------

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-08-19 06:26:24 +00:00
Sahil Bansal
52aa033fe4
add IsSuspicious value for reporting suspicious packages too (#567) 2025-08-18 17:56:24 +05:30
Kunal Singh
d8b83e2bc2
Merge pull request #566 from safedep/chore/misc-cleanup-20250815
chore: Misc cleanup and test improvements
2025-08-18 09:52:04 +05:30
Kunal Singh
b9ebcc71da
Merge branch 'main' into chore/misc-cleanup-20250815 2025-08-18 09:33:52 +05:30
Sahil Bansal
0f4c01b83a
add html reporter & create template for report (#559)
* add html reporter & create template for report

* updated table colors

* chore: rm unused code block

* add policy violations

* chore: rm extra var

* Update pkg/reporter/templates/report.templ

Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>

* chore: rm extra file

* chore: rm unsued css property

* add html reporter & create template for report

* updated table colors

* chore: rm unused code block

* add policy violations

* chore: rm extra var

* Update pkg/reporter/templates/report.templ

Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>

* chore: rm extra file

* chore: rm unsued css property

* return error when failing to create html reporter

---------

Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
2025-08-18 09:33:18 +05:30
abhisek
7cb923b2fd
fix: safely handle global config in test runner 2025-08-15 21:28:23 +05:30
abhisek
e32784a09e
fix: Test to use t.Setenv instead of os 2025-08-15 20:36:20 +05:30
abhisek
db6832e782
chore: Misc cleanup and test improvements 2025-08-15 20:26:55 +05:30
Kunal Singh
4b80c4a624
Fix: truffle hog, invalid commit hash. (#564)
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-08-13 08:49:43 +00:00
Sahil Bansal
12785f9c05
add support for publishing vet to npm (#563)
* add support for publishing vet to npm

* Update .github/workflows/publish-npm.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>

* update npm package readme

---------

Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-12 22:02:33 +05:30
Sahil Bansal
7d4569fb3d
use tag version instead of commit hash (#557) 2025-08-11 12:14:32 +00:00
Teja Kummarikuntla
47939fafaf
fix(report): Add commas between tags in generated markdown (#553)
* Seperate Tags with Comma in Report

* Delete pkg/.DS_Store

Signed-off-by: Teja Kummarikuntla <34749692+tejakummarikuntla@users.noreply.github.com>

* Update pkg/reporter/markdown.go

Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
Signed-off-by: Teja Kummarikuntla <34749692+tejakummarikuntla@users.noreply.github.com>

---------

Signed-off-by: Teja Kummarikuntla <34749692+tejakummarikuntla@users.noreply.github.com>
Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
2025-07-31 21:23:10 +05:30
Sahil Bansal
e68ead129b
add support for extensions purl (#551)
* add support for extensions purl

* Update pkg/common/purl/purl_test.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>

---------

Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-30 08:15:19 +05:30
Rohan Mishra
742365bc18
fix(tools): prevent nil pointer panic in GetPackageLicenseInfo handler (#548)
Signed-off-by: Rohan <315scisyb2020rohanmishra@gmail.com>
2025-07-27 18:51:59 +05:30
Kunal Singh
dee54e5184
Merge pull request #546 from safedep/deepwiki
added ask deepwiki badge.
2025-07-24 10:09:58 +05:30
Kunal Singh
1e84769891
added ask deepwiki badge.
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-07-23 21:42:10 +05:30
Kunal Singh
16a67216b4
Merge pull request #484 from safedep/fix/policy-violation-suspicious-#483
Fix/policy violation on  suspicious packages without paranoid mode #483
2025-07-22 10:27:18 +05:30
Abhisek Datta
0b4e76d858
Merge branch 'main' into fix/policy-violation-suspicious-#483 2025-07-22 10:11:36 +05:30
Abhisek Datta
ccd2c48e0c
fix: Misc cleanup of exclusion matcher initialization (#545) 2025-07-22 08:55:34 +05:30
Sahil Bansal
150cad94a6
Support exclusion patterns for lockfiles flag (#543)
* introduce config for lockfile reader

* add exclusion support

* add test cases for exclusion patterns

* refactor: introduce common exclusion matcher and update lockfile reader to use it

* chore: rm print statements

* refactor: use better naming for tests

* use doublestar lib for supporting dir reader exclusion patterns

* fix: path handling in exclusion matcher to support relative & absolute paths
2025-07-22 08:37:41 +05:30
Kunal Singh
c488d980cc fix: fail fast only on malware 2025-07-21 13:45:22 +05:30
Kunal Singh
3d8b7c5b63 feat: warning in markdown summary report for suspicious packages 2025-07-21 13:16:19 +05:30
Abhisek Datta
b4976630da
Merge branch 'main' into fix/policy-violation-suspicious-#483 2025-07-21 09:14:36 +05:30
Sahil Bansal
3d6d8ed036
Add github actions sync resolver (#539)
* feat: add GHA env resolver

* refactor: expose sync reporter resolver constructors

* fix: use os.LookupEnv for better GHA detection

* fix typo

* use environment sync resolver

* test: add test cases & fix naming

* Update pkg/reporter/sync_test.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>

* modify sync resolver tests

* fix tests failing in ci/cd

---------

Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 09:11:24 +05:30
Sahil Bansal
075627f53f
Add test cases for editor based extensions scanning (#542)
* fail fast if distribution is not among our supported editors

* tests: add test cases for each supported editor

* rm unused extensions
2025-07-19 00:17:47 +05:30
Abhisek Datta
1e2b75fa9c
Merge branch 'main' into fix/policy-violation-suspicious-#483 2025-07-17 09:29:18 +05:30
Sahil Bansal
06988f9b33
OpenVSX extensions scanning support (#536)
* feat(readers): Add OpenVSX ecosystem support

* refactor: use better naming conventions

* refactor: improve extensions reader with structured config
2025-07-15 18:40:02 +05:30
Abhisek Datta
c3d96dbef5
fix: Improve Agentic Query Prompt and Tools (#538) 2025-07-14 22:46:37 +05:30
Abhisek Datta
5f4cccbc85
feat: Add Support for Agentic Query and Analysis (#535)
* Add initial UI for agent mode

* fix: Cleanup and define agent contract

* Add react agent

* Add interactions memory

* Add support for stdio based MCP integration

* Add basic sqlite3 report generator

* fix: Persist vulnerabilities with package relation

* fix: Persist license information

* refactor: Agents into its own command package

* feat: Add support for tool calling introspection

* refactor: UI to hide implementation detail

* sqlite3 reporter persist dependency graph

* fix: Support multiple LLM provider for agent

* docs: Update agents doc

* docs: Remove deprecated query docs

* fix: UI tests

* fix: Linter issue

* Add support for prompt mode

* Improve UI with animation

* Fix UI tests after update

* Add OpenSSF scorecard persistence

* Add slsa provenances in sqlite3 reporter

* Add test cases for sqlite3 reporter

* Fix agent doc

* fix: Sqlite3 reporter use safe accessors

* feat: Add support for fast model

* feat: Simplify and streamline agent UI for better user experience

- Remove decorative borders and excessive styling to maximize output area
- Implement clean minimal design similar to modern TUI interfaces
- Add bordered input area for clear visual separation
- Move thinking indicator above input area for better visibility
- Enhance input field reset logic for proper line alignment
- Remove verbose help text and status messages
- Optimize layout calculations for full width utilization
- Add smooth animations for agent thinking state with spinner
- Clean up code structure and remove unused progress bar functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Improve agent status line

* test: Update UI tests

* fix: Use terminal safe rendering

* fix: Fix nil deref without storing empty strings in DB

* fix: Support overwriting sqlite3 database

* fix: Data model to use m2m between manifest and package

* style: Fix linter issue with unused variables

* Misc fixes

* Add test for agent memory

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-11 18:37:44 +05:30
Copilot
cd7caffb4a
Add HTTP HEAD request support to SSE MCP server (#533)
* Initial plan

* Add HTTP HEAD request support to SSE MCP server

- Created sseHandlerWithHeadSupport wrapper to handle HEAD requests to /sse endpoint
- HEAD requests return same headers as GET (text/event-stream, no-cache, etc.) without body
- Modified NewMcpServerWithSseTransport to use the wrapper
- Added comprehensive unit and integration tests
- Updated documentation to mention HEAD support for SSE endpoint
- Enables tools like Langchain to probe endpoint for health/capability checks

Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>

* Add HTTP HEAD request support to SSE MCP server

Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>

* Fix linter issues: remove trailing whitespace and handle w.Write error

Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>
2025-07-05 13:41:37 +00:00
Copilot
548ede77b8
Fix OSV report generation fallback value for 'introduced' version from "0.0.0" to "0" (#532)
* Initial plan

* Fix OSV introduced version fallback from 0.0.0 to 0 per OSV schema

Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: abhisek <31844+abhisek@users.noreply.github.com>
2025-07-04 11:47:10 +05:30
Kunal Singh
3fa7307d93
Merge pull request #529 from safedep/chore/sync-reporter-linter-fixes-cleanup
chore: Sync reporter linter fixes
2025-06-30 21:55:53 +05:30
abhisek
5cc80f9f88
chore: Sync reporter linter fixes 2025-06-30 21:46:50 +05:30
Omkar Phansopkar
387f6aeb72
Updated instructions for mcp server setup (#527)
* Updated instructions for mcp server setup

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Updated vscode usage image

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Spell vscode full form

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Added sse instructions

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

---------

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-06-28 18:13:48 +05:30
Omkar Phansopkar
200257bab3
Merge pull request #517 from safedep/chore/updateDeps
Updated deps and minor refactoring
2025-06-24 15:21:47 +05:30
Omkar Phansopkar
a87e6ab466
Updated deps
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-06-24 14:46:14 +05:30
Omkar Phansopkar
a0f6467e85
Merge branch 'main' into chore/updateDeps 2025-06-24 14:43:32 +05:30
Abhisek Datta
78e2bad49b
feat: Malicious Packages (OSV) Reporter for Inspect Command (#518)
* Add osv reporter

* fix: Pass config to openssf report generator

* fix: file name and check if already osv record exists' (#519)

---------

Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
2025-06-24 09:12:34 +00:00
Omkar Phansopkar
4f989c59f6
Fix e2e: scenario-11-code-csvreport.sh (#522)
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-06-24 14:39:02 +05:30
Omkar Phansopkar
932269d6bb
Updated contributing.md
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-06-19 20:22:04 +05:30
Omkar Phansopkar
7a2a365136
Updated testcase
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-06-14 15:44:29 +05:30
Omkar Phansopkar
459a246488
Updated docker go version
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-06-14 15:26:53 +05:30
Omkar Phansopkar
966971b941
Updated go version in CI workflows
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-06-14 15:16:33 +05:30
Omkar Phansopkar
f9d17487ad
Updated deps and minor refactoring
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-06-14 15:09:11 +05:30
Kunal Singh
8b71c540e6
feat: added cargo.lock scalibr parser (#512)
* feat: added cargo.lock scalibr parser

* fix: invalid manifest ref in cargo

* fix: list parser test - increment to 22

* fix: added cargo in supported ecosystems
2025-06-06 16:43:40 +05:30
Abhisek Datta
cccf646856
chore: Add multi-arch build for docker container (#510)
* chore: Add multi-arch build for docker container

* fix: Multi-platform build verification

* fix: Multi-platform build verification

* fix: MCP server docs

* chore: Add to cursor button for vet MCP Server
2025-06-06 12:59:05 +05:30
Kunal Singh
124199b331
docs(readme): added pkg.go.dev badge. (#509)
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-06-05 08:51:32 +00:00
Abhisek Datta
5a5a9518c6
feat: Add Support for vet MCP Server (#502)
* fix: MCP server with update mcp-go

* docs: Update MCP usage docs

* docs: Update MCP usage docs

* chore: Update DRY for Go adapter

* test: Add mcp driver test cases

* test: Simplify test cases

* docs: Update README

* docs: Update README

* test: Add test case for mcp tool

* test: Refactor for common concerns

* test: Add tool tests

* docs: Update MCP server docs

---------

Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
2025-06-05 11:00:43 +05:30
Sahil Bansal
3d94f0f710
Fixes duplicate & incorrect version in requirements.txt & empty Upgrade To Version Suggestion (#401)
* fixes #344

* fixed duplicate & incorrect version for requirements.txt

* fix return err if lfParser.Parse fails

* fix: Update edge cases and add test

---------

Co-authored-by: abhisek <abhisek.datta@gmail.com>
2025-06-04 09:44:08 +05:30
Kunal Singh
4f43177976
fix: pomxml parser not working when renamed files (#505)
* fix: pomxml parser not working when renamed files

* fix: pom.xml parsing with differnet filename

* removed test file'

* remvoed unused code
2025-06-03 13:49:32 +05:30
Kunal Singh
0a2d642ea8
Docs: added instructions for container image scanning & and fix misc (#501)
* fix: scan flgas, report-cdx and report-graph

* added more instructions for container image scanning
2025-05-30 17:40:47 +05:30
Kunal Singh
49cc6ca395
Support for local docker and tar images in container scanning. (#497)
* refactor: convert current image resolution into workflow pattern

* feat: image from local tarball folder

* feat: image from local docker catalog

* refacot: decompose docker image catalog resolver into multiple functions

* refactor: using utilit function for logging and return error

* feat: remove tem tar dir after image obj is created

* refactor: handle empty nil error in log and error funciton

* removed local image not supported test

* fix: error fmt.Errorf with non-constant values

* go mod tidy

* fix: linter unreachable  code

* refactor: removed unwanted parameter

* test: added scenerio for different image scan operations

* fix: added scenario into all.sh

* fix: missed .sh extension for one entry

* fix: test bad file path for local tar scan

* remvoed logger and error combined function

* refactor: using context form top of tree

* feat: using custom error for image resolution unsupported

* refactor: returning unsupported workflow error for each docker api fail

* feat: added no-remote flag for disable remote fetch

* feat: --image-no-remote

* fix: test, creating temp files witn \/ causing issue

* chore: Misc cleanup for Container Image Resolver (#499)

* chore: Misc cleanup

* fix: Bug with docker image resolver

* fix: Error msg

* chore: Improve debug logging for docker enumeration

* chore: Improve debug logging for docker enumeration

---------

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-05-29 22:36:49 +05:30
Abhisek Datta
a2c003f634
docs: Update README (#496) 2025-05-29 00:03:48 +05:30
Abhisek Datta
72e08bdd8a
refactor: Sync reporter to allow env resolver adapter (#495)
* refactor: Sync reporter to allow env resolver adapter

* fix: Set optional params only when not empty

* fix: linter warning
2025-05-27 22:00:50 +05:30
Kunal Singh
1f8a5750d2
feat: container scanning (#489)
* feat: container scanning

* fix: tests

* refactor: added commens and set running-system for remote image to false

* using standalone extractors with all scope

* feat: handle manifects

* using set for duplicate purl

* feat: added initial tests

* refactor: creating manifest from local and modified tests

* refactor: decouple parser and reader for container scanning

* refactor: seperated image and reader config

* refactor: using applicatoin name to purl

* refactor: removed technical osv-scalibr names from error message

* refactor: misc

* added analytics

* moved container image reader test to e2e test

* refactor: getting image in consutrctor

* refactor: composit grouping of ecosystem and file

* feat: cache to reudce duplicate packages

* refactor: removed json-dump-dir output dir

* feat: added ui for better UX

* test: skip when not e2e

* refactor: made scalibr image object reference private

* test: application name testing'

* test, different image with invalid cases

* feat: clean up image after use

* refactor: fetching image in enum manifests

* fix: tests, handing error from enum manifests

* tests: added test for local image not supported

* refactor: removed ui reference in container reader
2025-05-27 20:45:37 +05:30
Kunal Singh
826b8eafdf
Fix/ Bug in Resolving Package Version in Maven POM with Dependency Management (#487)
* feat: pom.xml dependency resolver

* feat: manifest from packages

* feat: disable osv-scalibr's native loggin

* fix: parser test, update total parsers count'

* refactor: using manifest's NewPackageManifestFromLocal and AddPackage methods

* fix: tests, package enumertaion with new pom.xml parser

* fix: docker image with go 1.24.2

* feat(test): pom parser

* refactor: extracted commaon osv-scalibr's function in scalibr.go

* refactor: setting scalibr logger in init function
2025-05-20 18:10:49 +05:30
Omkar Phansopkar
5c7ab43567
Merge pull request #482 from safedep/fix/481-sarif-report-builder
fix: SARIF report builder to handle vuln and malicious code rule index
2025-05-14 12:32:43 +05:30
Kunal Singh
a04cf78657 refactor: simplified 2025-05-09 16:19:41 +05:30
Kunal Singh
9e9abdd162 fix: tests, common pkgManifest causing issues due to multiple packages 2025-05-09 15:58:27 +05:30
Kunal Singh
5840ebd227 test: check if policy is violated 2025-05-09 11:47:37 +05:30
Kunal Singh
f2a2eb0548 fix: unwanted policy violation emmition for suspicious packages 2025-05-08 20:45:55 +05:30
abhisek
ac1c83393f
fix: SARIF report builder to handle vuln and malicious code rule index 2025-05-05 17:55:23 +05:30
Abhisek Datta
a77be8f4c4
feat: Add manifest source in csv report (#480) 2025-04-28 23:11:55 +05:30
infosecwonderland
eebae09e82
feat: add excludeRepos support to GithubOrgReader (#476)
* feat: add excludeRepos support to GithubOrgReader

* feat: add excludeRepos support to GithubOrgReader

* Syntax changes

* fix: excluded repo variable declartion

* fix: scan command to include excluded repo config

---------

Co-authored-by: abhisek <abhisek.datta@gmail.com>
2025-04-28 15:03:54 +05:30
Omkar Phansopkar
e90756d5a3
CDX reporting in readme (#479)
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-04-28 12:00:47 +05:30
Abhisek Datta
c795255e35
docs: Update vet logo (#475)
* docs: Update vet logo

* fix: logo size

* docs: Improve README by using collapsible sections

* docs: Add interactive tutorial

* docs: Update vet logo

* docs: Update README

* docs: Update README

* docs: Update vet logo

* docs: Update README

* fix: Revert star history URL

* fix: README image
2025-04-24 23:56:02 +05:30
Abhisek Datta
36d5c021f3
fix: Handle github reader reading directory (#477) 2025-04-24 22:24:24 +05:30
Abhisek Datta
3490812ed1
chore: Add anonymous telemetry collector (#468)
* chore: Add anonymous telemetry collector

* fix: Posthog property handling
2025-04-22 15:53:32 +05:30
Omkar Phansopkar
5b766bb27b
Merge pull request #465 from safedep/feat/add-query-malysis-enricher
feat/add query malysis enricher
2025-04-21 11:16:40 +05:30
abhisek
0bdbf2da8a
test: Add test case for malware query enricher 2025-04-20 13:30:56 +05:30
abhisek
00eb5c8ec7
feat: Update markdown summary reporter for malware query 2025-04-20 10:45:59 +05:30
abhisek
0a3fc8d428
chore: Update dependencies 2025-04-19 22:15:51 +05:30
abhisek
3d7ea62f61
feat: Add malware query enricher 2025-04-19 22:11:17 +05:30
abhisek
88f5178a05
feat: Add community service API endpoint 2025-04-19 20:49:52 +05:30
Kunal Singh
3d9639d0ef
feat: github repos version resolution (#458)
* feat: github repos version resolution

* fix: malware analysis e2e testing

* fix: e2e testing for malware analysis using vet scan

* revert: e2e testing

* fix(test): remove bad e2e tests commands, scan without version has not effect

* fix: test failing 🔥, for scoped packages

* feat(e2e): added inspect malware command

* fix: use of internal code to library

* refactor: Maintain separation of concerns and loose coupling

* fix: PURL reader test

---------

Co-authored-by: abhisek <abhisek.datta@gmail.com>
2025-04-12 08:05:01 +05:30
Nilanjan De
03e1a10c1d
chore: correct typos (#460) 2025-04-11 21:19:02 +00:00
Abhisek Datta
7f88f83a8c
ci: Fix codecov upload step (#457) 2025-04-10 16:23:19 +05:30
Kunal Singh
0d1ba75d4c
feat(inspect): resolve version of npm, pypi, rubygem packages' (#451)
* feat(inspect): resolve version of npm, pypi, rubygem packages'

* chore: go mod tidy

* feat: added version resolve to purl reader and added tests

* feat(inspect): resolve version of npm, pypi, rubygem packages'

* fix: updated safedep/dry version

* fix: removed unused block of code, unremoved after refactoring

* refactor: remove custom registry adapter function with predefined

* fix: error handing

* refactor: print statement for better aligning with current code

* refactor: added helpful error message

* Update cmd/inspect/malware.go

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* Update pkg/common/packageregistry.go

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* feat(e2e test): malware analysis without version

---------

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-04-09 14:39:30 +05:30
Abhisek Datta
2e0c4a5d3d
ci: Add codecov reporting (#454) 2025-04-09 11:46:22 +05:30
Omkar Phansopkar
923fc4744c
Implemented CycloneDX reporter with metadata, packages & vulnerabilities (#434)
* Implemented CycloneDX reporter with metadata, packages & vulnerabilities

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Refactor to using PtrTo instead of dereferencing

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Minor lint fixes

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Implemented CycloneDX features - Licenses, Vulnerability & annotations

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Support malware in cyclonedx bom

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Script for SPDX licenses, prevent duplicate vulnerabilities

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Fix comment typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Test cases for reader application names

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Replaced StringPtr with PtrTo

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Tests for cyclonedx reporter and cvss score calculation

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

---------

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-08 19:54:50 +05:30
Omkar Phansopkar
859ce29ab0
Fixed incorrect dependencies provided by Package.GetDependencies (#450)
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-04-07 13:44:08 +05:30
Kunal Singh
0ad855f2a8
Feat/gitlab policy violations (#445)
* feat: added policy violation support in gitlab reporting

* feat(gitab): policy violations entries in report

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* fix(gitlab): identifier id

* fix: using info severity level for policy violations

---------

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-04-07 12:00:40 +05:30
Arunanshu Biswas
f6797d0d6f
feat(reporter): sync malware analysis report (#444)
* build(deps): update dependencies

* feat(reporter): sync malware analysis report

* refactor: fix verification record

* test(reporter/sync): add basic tests for sync
2025-04-03 20:23:30 +05:30
Kunal Singh
6cf88c2e86
Merge pull request #441 from safedep/feat/#430-sarif-vuln-malware
Added vulnerabilities & malware in SARIF reports
2025-04-02 10:50:58 +05:30
Omkar Phansopkar
aaa1794e89
Removed unnecessary debug log
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-04-02 10:38:23 +05:30
Omkar Phansopkar
0bfd37027f
Fix incorrect summary for Malware analyzer event filters
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-04-02 10:32:40 +05:30
Omkar Phansopkar
d7f7a6c72e
Use common ToolMetadata for all reporters and default-enable vuln & malware in SARIF
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-04-01 21:10:45 +05:30
Omkar Phansopkar
976c5317ac
Fix uninitialised vulncache
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-04-01 19:58:59 +05:30
Omkar Phansopkar
86382bbc70
Refactor tool meta data config & using separate vulncache
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-04-01 19:52:08 +05:30
Omkar Phansopkar
d0111cec20 Added vulnerabilities & malware in SARIF reports
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-03-31 20:59:29 +05:30
Abhisek Datta
95c2970e6d
fix: Do not delete old container images (#440) 2025-03-31 11:31:39 +00:00
Kunal Singh
bf2316843b
feat: Improve Description and Solution for Vulnerabilities (#439)
* feat: better descritpion and sulution for vulns

* fix: missing check for nil
2025-03-31 15:19:02 +05:30
Kunal Singh
02fa8de1b4
docs(readme): updated gitlab ci component link (#436) 2025-03-30 18:23:01 +05:30
Omkar Phansopkar
828467309c
Fixed plaintext in sarif messages (#435)
* Fixed plaintext in sarif messages

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Fixed extra group items in regex replace

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Updated testcase with misc symbols

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

---------

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-03-29 19:27:17 +05:30
Kunal Singh
c156f54274
Feat/gitlab report (#419)
* feat: new gitlab report

* fix: gitlab-report version missing

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* fix: isseues with schema

* added comments

* fix: removed unknown identifiers

* fix: direct dependency suing depth == 0

* oops: pushed output file :)

* fix: identifiers was taken only one eacy type

* fix: nested struct on GitLabReport

* added relavent docs reference

* fix: hardcoded vet version, with correct version from version.go

* added malwares

* refactor: minor

* fix: summary and name

* added tests

* refactor: seperate function for severity

* refactor: using enums for severity

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* feat: using all gitlab identifiers

* refactor: removed unwanted package struct exports

* refactor: IsDirect function to check if package is direct dependency

* removed TODO comments

* refactor: using non-standard id for malware analysis

* refactor: removed hardcoded valuesin gitlab.go

* refactor: gitlab consts

* feat: add gitlab scan fail status const

* refactor: extracted identifiers urls

* fix: github adivisory link

* refactor: solid principle for identifiers url, and scoped method for gitlab reporter

---------

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-03-26 20:26:05 +05:30
Abhisek Datta
f6258fdc86
feat: Add support for malysis min confidence config (#429)
* feat: Add support for malysis min confidence config

* fix: Test case to use factory function
2025-03-26 14:07:40 +05:30
Kunal Singh
635baeb86e
feat: test container working: scan with cloud (#424)
* feat: test container working: scan with cloud

* removed testing policy.yml file

* feat: container test using auth verify

* fix: ci fail due to missing container tag

* fix: buildx not storing images locally

* feat: envs into image

* let see :)

* using secrets directly into docker command

* why do i need to mount?

* fix: test run only on on pr to main repo
2025-03-26 12:43:59 +05:30
Abhisek Datta
6eec7e1740
fix: Dockerfile install ca-certificates (#422) 2025-03-25 09:59:34 +05:30
Kunal Singh
18b9ffee84
fix: container permission issue when creating report files (#417) 2025-03-24 14:21:28 +05:30
Kunal Singh
ed80658f46
Merge pull request #413 from safedep/fix/vet-version-error-code
fix: error code in vet version command
2025-03-23 09:07:31 +05:30
Kunal Singh
21a41aa3e8
Merge branch 'main' into fix/vet-version-error-code 2025-03-23 09:04:59 +05:30
Abhisek Datta
05e6fbebfd
ci/harden github actions (#412)
* ci: Pin github actions to its commit SHA

* ci: Pin github actions to its commit SHA

* chore: Pin docker base images

* fix: Typo in vet-ci
2025-03-23 01:46:25 +05:30
Kunal Singh
c8d4cd38c9 fix: error code in vet version command 2025-03-22 16:46:21 +05:30
Abhisek Datta
35b0021569
Update container.yml (#410)
Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-03-21 16:22:43 +05:30
Abhisek Datta
4be215d914
ci: tag container image release (#407)
Co-authored-by: Kunal Singh <kunalsin9h@gmail.com>
2025-03-21 16:13:51 +05:30
Kunal Singh
c1d9050d26
Merge pull request #409 from safedep/fix/nonroot-user-in-debian
fix: use of default non-root user outside distroless images
2025-03-21 16:08:50 +05:30
Kunal Singh
14ca44453f fix: use of default non-root user outside distroless images
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-03-21 16:04:40 +05:30
Kunal Singh
2c682bdf66
fix: missing sheel for using vet's image as environment (#408)
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-03-21 15:29:05 +05:30
Omkar Phansopkar
855f0afb21
Updated code with fix need for go runtime (#406)
Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-03-21 11:51:02 +05:30
Abhisek Datta
49b2e0f3df
feat: Add Support for GitHub Action or Repository Scanning (#405)
* feat: Add support for github actions scanning

* fix: enrich malware test cases

* fix: fail fast for malware inspect if auth not available

* fix: bug with package version
2025-03-21 04:23:47 +05:30
Sahil Bansal
5387a395a3
fix uv.lock Parser Graph Root Handling (#397)
* fix uv.lock Parser Graph Root Handling

* fixed test case for pathToRoot

* added support for dev dependencies & extra support fot scanning root pkg

---------

Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-03-20 10:31:55 +05:30
Abhisek Datta
ec141c3693
Update README.md (#402)
* Update README.md

Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>

* Update README.md

Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>

* Update README.md

Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>

---------

Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-03-19 14:28:49 +05:30
Kunal Singh
e0bb4a7836
feat: progress bar in cloud report syncing (#400)
* feat: progress bar in cloud report syncing

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* fix: missing nil check guard in syncReportTracker closure

* Update pkg/reporter/sync.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* fix: race condition of pre-closure of progress bar before finish

* fix: race condition

* fix: Delay marking trackers as done till stop event

---------

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: abhisek <abhisek.datta@gmail.com>
2025-03-19 09:36:27 +05:30
Kunal Singh
27548de0c8
fix: Handling Package Dependency Callback for Insights v2 Package Met… (#393)
* fix: Handling Package Dependency Callback for Insights v2 Package Meta Enricher

* fix: unwanted newline character and ecosystem handing
2025-03-17 21:15:06 +05:30
Omkar Phansopkar
666011a975
Implemented Defectdojo reporter (#388)
* Implemented Defectdojo reporter

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Added defect-dojo reporter to query command

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Accept defectdojo host URL as arg instead of environment variable

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

---------

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-03-17 11:55:22 +05:30
Sahil Bansal
5b4ae39c6a
Feat/#374 support for scanning uv.lock files (#389)
* implemented custom parser for uv.lock files

* ecosystem fixes

* Added tests for uv lockfile parsing

* Incremented list parser count
2025-03-17 08:49:10 +05:30
Kunal Singh
9bd1fb019b
fix: missing version on go install builds (#387)
* fix: missing version information while building using go install

* refactor: missing version on go install builds

* refactor: go version set by both ldflags and go install(runtime)

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

---------

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-03-13 20:54:10 +05:30
Abhisek Datta
1e631efb06
chore: Remove linter-install from dev-setup (#386) 2025-03-12 16:15:31 +05:30
Kunal Singh
5f960dbf6c
Migrated go version to 1.24 (#381)
* Migrated project to go1.24

* using oapi-codegen from go tools

* tools section to the top in go.mod

* using 1.10.1 oapi-codegen version

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* using actions commit hash inplace of version

* revert back to go install for tool oapi-codegen-install

* refactor(devtools): removed oapi-codegen from Makefile

* fix(vet): update gomarkdown package

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>

* fix(vet): updated gin-contrib/sse package

---------

Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-03-12 16:01:24 +05:30
Abhisek Datta
c9bb677999
ci: vet enable comments proxy (#382) 2025-03-10 17:20:37 +05:30
Kunal Singh
fdb3bdd0f2
Add ASDF plugin installation steps to CONTRIBUTING.md (#380)
Signed-off-by: Kunal Singh <kunalsin9h@gmail.com>
2025-03-10 08:16:36 +00:00
Omkar Phansopkar
ec02875f75
Updated code analysis framework (#370)
* Updated code analysis framework

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

* Updated code with latest packagehint resolution

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>

---------

Signed-off-by: Omkar Phansopkar <omkarphansopkar@gmail.com>
2025-03-07 09:52:56 +05:30
Abhisek Datta
2f4e02e883
refactor: Remove deprecated docs (#371) 2025-03-04 19:47:28 +05:30
Sachin Maurya
71fce1b1b2
fix: remove replace directive from go.mod file (#369)
Signed-off-by: slayer321 <sachin.maurya7666@gmail.com>
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
2025-03-04 13:44:22 +05:30
Abhisek Datta
1864e687f8
feat/vscode malysis support (#368)
* chore: Update API sdk to add support for vsx scanning

* chore: Add inspect cmd flag to skip waiting for result

* feat: Add support for VS Code extension reader

* fix: Remove unnecessary vsx distribution path
2025-03-04 13:43:09 +05:30
Abhisek Datta
d599ac8407
feat: Add vet cloud quickstart command (#361)
* feat: Add vet cloud quickstart command

* fix: Quick start tenant setup

* fix: Display msg for API key creation
2025-02-27 04:24:49 +05:30
Abhisek Datta
1db3d0d2cf
fix: Update SLSA provenance generator to handle GitHub deprecated action (#360) 2025-02-25 13:40:21 +05:30
Abhisek Datta
359712bee0
fix: Handle known false positives with npm LFP (#359) 2025-02-25 11:25:58 +05:30
Abhisek Datta
c313485e2f
feat: Allow summary report to filter by usage evidence (#354)
* feat: Allow summary report to filter by usage evidence

* fix: Filter by dependency usage evidence
2025-02-19 23:17:35 +05:30
Abhisek Datta
99612906d8
fix: Summary report tag refactoring (#349)
* fix: Summary report tag refactoring

* chore: Dependency upgrades
2025-02-17 19:13:48 +05:30
518 changed files with 83358 additions and 25199 deletions

24
.cursor/mcp.json Normal file
View File

@ -0,0 +1,24 @@
{
"mcpServers": {
"pinner-mcp-stdio-server": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"ghcr.io/safedep/pinner-mcp:latest"
]
},
"vet-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"ghcr.io/safedep/vet:latest",
"server",
"mcp"
]
}
}
}

View File

@ -0,0 +1,10 @@
---
description:
globs:
alwaysApply: true
---
Always use vet-mcp to vet any open source library package suggested by AI generated code before installation.
Follow the rules below to use vet-mcp:
- Try to find the latest version. If not found, it is likely a slopsquatting vulnerability
- Always check for critical vulnerabilities and malware

View File

@ -3,29 +3,60 @@ on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read
jobs:
check-generated-code:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
with:
go-version-file: go.mod
- name: Run code generation
run: make generate
- name: Check for uncommitted changes
run: |
if [[ -n $(git status --porcelain) ]]; then
echo "ERROR: Generated code is out of sync!"
echo "Please run 'make generate' and commit the changes."
echo ""
echo "Files with changes:"
git status --porcelain
echo ""
echo "Diff:"
git diff
exit 1
fi
run-test:
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v3
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
with:
go-version: 1.23
check-latest: true
go-version-file: go.mod
- name: Build and Test
run: |
go mod tidy
go build
go test -v ./...
go test -coverprofile=coverage.txt -v ./...
env:
VET_E2E: true
@ -33,17 +64,23 @@ jobs:
# test suites that use GitHub API
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Coverage
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push'
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
run-e2e:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v3
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
with:
go-version: 1.23
go-version-file: go.mod
check-latest: true
- name: Build vet
@ -68,21 +105,34 @@ jobs:
VET_API_KEY: ${{ secrets.SAFEDEP_CLOUD_API_KEY }}
VET_CONTROL_TOWER_TENANT_ID: ${{ secrets.SAFEDEP_CLOUD_TENANT_DOMAIN }}
build-container:
build-container-test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout Source
uses: actions/checkout@v3
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Build Container Image
- name: Build Multi-Platform Container Image (verification only)
run: |
docker buildx build --platform linux/amd64 --platform linux/arm64 \
-t build-container-test .
docker buildx build --platform linux/amd64,linux/arm64 \
-t build-container-test:latest .
- name: Build and Load Native Platform Image for Testing
run: |
docker buildx build --platform linux/amd64 --load \
-t build-container-test:latest .
- name: Test Container Image
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
run: |
docker run --rm \
-e VET_API_KEY=${{ secrets.SAFEDEP_CLOUD_API_KEY }} \
-e VET_CONTROL_TOWER_TENANT_ID=${{ secrets.SAFEDEP_CLOUD_TENANT_DOMAIN }} \
build-container-test:latest \
auth verify

View File

@ -35,20 +35,20 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
language: ["go"]
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34
with:
go-version: 1.23
go-version-file: go.mod
check-latest: true
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -63,6 +63,6 @@ jobs:
go build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@b8d3b6e8af63cde30bdc382c0bc28114f4346c88 # v2
with:
category: "/language:${{matrix.language}}"

View File

@ -27,28 +27,130 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744
with:
submodules: true
fetch-depth: 0
- name: Registry Login
uses: docker/login-action@v1
uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55
- name: Build and Push Container Image
run: |
docker buildx build --push --platform linux/amd64 --platform linux/arm64 \
# Get the tag if this was a tag push event
if [[ "${{ github.ref_type }}" == "tag" ]]; then
TAG=${{ github.ref_name }}
# Validate tag format (must be vX.Y.Z)
if [[ $TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
# Build and push with both version tag and latest
docker buildx build --push --platform linux/amd64,linux/arm64 \
-t $REGISTRY/$IMAGE_NAME:$TAG \
-t $REGISTRY/$IMAGE_NAME:latest \
.
else
echo "Invalid tag format. Must be in format vX.Y.Z (e.g. v1.2.3)"
exit 1
fi
else
# For non-tag pushes, just use latest tag
docker buildx build --push --platform linux/amd64,linux/arm64 \
-t $REGISTRY/$IMAGE_NAME:latest \
.
fi
publish-mcp-registry:
if: startsWith(github.ref, 'refs/tags/') # only run this when new tag is publish
needs: build
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC authentication
contents: read
defaults:
run:
working-directory: ./.mcp-publisher
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Ensure jq is installed
run: sudo apt-get update && sudo apt-get install -y jq
- name: Get version from tag
# Strip 'v' prefix from tag (e.g., v1.0.0 -> 1.0.0) as
# - we want clean version (x.y.z) without v prefix, since its already added by registry UI
# - in case of docker image, we hardcode in server.json docker image identifier
run: echo "VET_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: fill version in server.json
run: sed -i "s/VERSION_FROM_ENV/$VET_VERSION/g" server.json
# publish mcp server
- name: Install mcp-publisher
run: |
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
- name: Authenticate to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Publish server to MCP Registry
run: ./mcp-publisher publish
verify-publish-mcp-registry:
needs: publish-mcp-registry
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Ensure jq is installed
run: sudo apt-get update && sudo apt-get install -y jq
- name: Get version from tag
# Strip 'v' prefix from tag (e.g., v1.0.0 -> 1.0.0) as
# - we want clean version (x.y.z) without v prefix, since its already added by registry UI
# - in case of docker image, we hardcode in server.json docker image identifier
run: echo "VET_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Query MCP Registry and verify server is published
env:
SERVER_NAME: "io.github.safedep/vet-mcp"
REGISTRY_URL: "https://registry.modelcontextprotocol.io/v0.1/servers"
run: |
export EXPECTED_VERSION=$VET_VERSION
echo "Checking MCP Registry for $SERVER_NAME"
# Query registry
url="${REGISTRY_URL}?search=${SERVER_NAME}"
echo "Requesting: $url"
http_status=$(curl -s -o response.json -w "%{http_code}" "$url")
if [ "$http_status" -ne 200 ]; then
echo "Registry query failed with HTTP status $http_status"
cat response.json || true
exit 1
fi
# Pretty print the response for debugging
echo "Registry response (truncated):"
jq 'if .servers then {servers: (.servers | length)} else . end' response.json
# Check for name and version match
jq -e --arg name "$SERVER_NAME" --arg ver "$EXPECTED_VERSION" 'any(.servers[]; .server.name == $name and .server.version == $ver)' response.json >/dev/null || {
echo "ERROR: Server $SERVER_NAME with version $EXPECTED_VERSION not found"
echo "Full response:"
cat response.json
exit 1
}
echo "Found server $SERVER_NAME with version $EXPECTED_VERSION"

View File

@ -4,7 +4,7 @@
#
# Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
name: 'Dependency Review'
name: "Dependency Review"
on: [pull_request]
permissions:
@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v3
- name: "Checkout Repository"
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: "Dependency Review"
uses: actions/dependency-review-action@cc4f6536e38d1126c5e3b0683d469a14f23bfea4 # v3

65
.github/workflows/gh-pages-deploy.yml vendored Normal file
View File

@ -0,0 +1,65 @@
name: CLI Reference Manual GitHub Pages Deploy
on:
push:
branches:
- main
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
env:
SOURCE_GEN_DIR: ./docs/manual
jobs:
# Build Jekkll (md -> html)
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
with:
go-version-file: go.mod
- name: Build vet
run: go build
- name: Generate MD Docs in ${{ env.SOURCE_GEN_DIR }}
run:
./vet doc generate --markdown ${{ env.SOURCE_GEN_DIR }}
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ${{ env.SOURCE_GEN_DIR }}
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
# Deployment job
deploy:
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@ -6,23 +6,22 @@ on:
permissions:
contents: read
pull-requests: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/setup-go@v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
with:
go-version: 1.23
cache: false
- uses: actions/checkout@v3
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
with:
version: latest
args: --issues-exit-code=1 --timeout=10m
only-new-issues: true
skip-pkg-cache: true
skip-build-cache: true

View File

@ -3,7 +3,7 @@ name: Release Automation
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- "v[0-9]+.[0-9]+.[0-9]+"
concurrency: ci-release-automation
@ -34,9 +34,9 @@ jobs:
- uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2
- uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 # v2
- name: Set up Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34
with:
go-version: 1.23
go-version-file: go.mod
check-latest: true
- name: ghcr-login
uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7 # v1
@ -46,7 +46,7 @@ jobs:
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
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: |
@ -56,7 +56,7 @@ jobs:
- name: Setup Cache for OSX Cross Compiler Tool Chain
id: osxcross-cache
uses: actions/cache@v3
uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f # v3
with:
key: ${{ runner.os }}-osxcross-${{ env.OSX_CROSS_MACOS_SDK_VERSION }}
path: |
@ -75,27 +75,31 @@ jobs:
uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0
with:
distribution: goreleaser
version: '~> v2'
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
- name: Upload dist folder
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist-artifacts
path: dist/
- name: Generate subject
id: hash
env:
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
run: |
set -euo pipefail
checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT"
provenance:
needs: [goreleaser]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
attestations: write # To write attestations
runs-on: ubuntu-latest
steps:
- name: Download dist folder
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
upload-assets: true
private-repository: false
name: dist-artifacts
path: dist/
- name: Attest build provenance (checksums)
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-checksums: dist/checksums.txt

63
.github/workflows/publish-npm.yml vendored Normal file
View File

@ -0,0 +1,63 @@
name: Publish NPM Package
on:
workflow_run:
workflows: ["Release Automation"]
types:
- completed
jobs:
publish-npm:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
contents: read
id-token: write
outputs:
package_version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "18"
registry-url: "https://registry.npmjs.org"
- name: Extract Tag Version
id: version
run: |
TAG_VERSION="${{ github.event.workflow_run.head_branch }}"
if [[ "$TAG_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="${TAG_VERSION#v}" # Remove leading 'v'
echo "version=$VERSION" >> $GITHUB_OUTPUT
else
echo "No valid tag found in head_branch: $TAG_VERSION"
exit 1
fi
- name: Prepare package
run: |
cd publish/npm
npm version ${{ steps.version.outputs.version }} --no-git-tag-version
- name: Publish to npm
run: |
cd publish/npm
npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
test-installation:
needs: publish-npm
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: ["14", "18", "20"]
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Test installation
run: |
npm install -g @safedep/vet@${{ needs.publish-npm.outputs.package_version }}
vet version
vet --help || true

View File

@ -10,7 +10,7 @@ on:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '35 22 * * 0'
- cron: "35 22 * * 0"
push:
branches: ["main"]

View File

@ -13,13 +13,13 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout Source
uses: actions/checkout@v2
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5
with:
fetch-depth: '0'
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@main
uses: trufflesecurity/trufflehog@8b6f55b592e46ac44a42dc3e3dee0ebcc0f56df5
with:
path: ./
base: main
head: HEAD
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}

View File

@ -20,9 +20,9 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Enable Cloud Cloud
- name: Enable Cloud Mode
run: echo "SAFEDEP_CLOUD_MODE=true" >> $GITHUB_ENV
- name: Override Cloud Mode if Actor is Dependabot
@ -34,12 +34,13 @@ jobs:
run: echo "SAFEDEP_CLOUD_MODE=false" >> $GITHUB_ENV
- name: Run vet
uses: safedep/vet-action@v1
uses: safedep/vet-action@01f547ee95dfd4f8f11fa64b399e5e00f22b0801
with:
policy: .github/vet/policy.yml
cloud: ${{ env.SAFEDEP_CLOUD_MODE }}
cloud-key: ${{ secrets.SAFEDEP_CLOUD_API_KEY }}
cloud-tenant: ${{ secrets.SAFEDEP_CLOUD_TENANT_DOMAIN }}
enable-comments-proxy: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SAFEDEP_CLOUD_MODE: ${{ env.SAFEDEP_CLOUD_MODE }}

View File

@ -0,0 +1,36 @@
name: Container Scan E2E
on:
push:
branches: [ main ]
pull_request:
permissions: read-all
jobs:
e2e-scan:
name: E2E Scan on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Checkout Source
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
with:
go-version-file: go.mod
- name: Build and Test
run: |
make
- name: Run container scan tests
shell: bash
run: |
./vet scan --image alpine:latest
./vet scan --image ghcr.io/safedep/vet:latest
./vet scan --image node:20

7
.gitignore vendored
View File

@ -22,3 +22,10 @@
/vet
dist/
/.env.dev
.vscode/
# MacOS specific files
**/.DS_Store
# Auto-generated context files
CLAUDE.md

18
.golangci.yml Normal file
View File

@ -0,0 +1,18 @@
# golangci-lint configuration file
# See https://golangci-lint.run/usage/configuration/
version: "2"
linters:
exclusions:
paths: []
formatters:
enable:
- gci
- gofumpt
settings:
gci:
sections:
- standard
- default
- localmodule

View File

@ -12,15 +12,26 @@ env:
builds:
- id: linux
goos: [linux]
goarch: [amd64]
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++
@ -28,6 +39,7 @@ builds:
goos: [windows]
goarch: [amd64]
env:
- CGO_ENABLED=1
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
@ -67,7 +79,7 @@ archives:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
name_template: "checksums.txt"
algorithm: sha256
snapshot:
version_template: "{{ incpatch .Version }}-next"
@ -75,9 +87,8 @@ 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

@ -0,0 +1,74 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
"name": "io.github.safedep/vet-mcp",
"title": "SafeDep Vet MCP",
"description": "Protect your AI agents and IDEs from malicious open-source packages.",
"version": "VERSION_FROM_ENV",
"websiteUrl": "https://safedep.io",
"repository": {
"url": "https://github.com/safedep/vet",
"source": "github"
},
"icons": [
{
"src": "https://raw.githubusercontent.com/safedep/.github/9275c7d1b59f718d73e47cecd93df92e7bfbea25/assets/logo/safedep-logo-darkshade.svg",
"mimeType": "image/svg+xml",
"sizes": [
"48x48",
"96x96"
],
"theme": "light"
},
{
"src": "https://raw.githubusercontent.com/safedep/.github/9275c7d1b59f718d73e47cecd93df92e7bfbea25/assets/logo/safedep-logo.svg",
"mimeType": "image/svg+xml",
"sizes": [
"48x48",
"96x96"
],
"theme": "dark"
}
],
"packages": [
{
"registryType": "oci",
"identifier": "ghcr.io/safedep/vet:vVERSION_FROM_ENV",
"runtimeHint": "docker",
"runtimeArguments": [
{
"type": "named",
"name": "--rm",
"value": ""
},
{
"type": "named",
"name": "-i",
"value": ""
}
],
"packageArguments": [
{
"type": "positional",
"value": "-s"
},
{
"type": "named",
"name": "-l",
"value": "/tmp/vet-mcp.log"
},
{
"type": "positional",
"value": "server"
},
{
"type": "positional",
"value": "mcp"
}
],
"transport": {
"type": "stdio"
}
}
]
}

View File

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

View File

@ -3,7 +3,7 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go",
"editor.codeActionsOnSave": {
"source.organizeImports": true
"source.organizeImports": "explicit"
}
},
"gopls": {

View File

@ -25,35 +25,38 @@ Create a new issue and add the label "enhancement".
When contributing changes to repository, follow these steps:
1. Ensure tests are passing
2. Ensure you write test cases for new code
3. `Signed-off-by` line is required in commit message (use `-s` flag while committing)
1. If you modified code that requires generation (e.g., enum registrations, ent schemas), run `make generate` and commit the generated files
2. Ensure tests are passing
3. Ensure you write test cases for new code
4. `Signed-off-by` line is required in commit message (use `-s` flag while committing)
## Developer Setup
### Requirements
* Go 1.22+
- Go 1.25.0+
### Install Dependencies
* Install [ASDF](https://asdf-vm.com/)
* Install the development tools
- Install [ASDF](https://asdf-vm.com/)
- Install the development tools
```bash
asdf plugin add golang
asdf plugin add gitleaks
asdf install
```
* Install `lefthook`
- Install git hooks (using Go toolchain)
```bash
go install github.com/evilmartians/lefthook@latest
go tool github.com/evilmartians/lefthook install
```
* Install git hooks
Install `golangci-lint`
```bash
$(go env GOPATH)/bin/lefthook install
```shell
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
```
### Build
@ -76,11 +79,24 @@ Quick build without regenerating code from API specs
make quick-vet
```
### Generate Code
If you modify code that requires generation (enum registrations in `pkg/analyzer/filterv2/enums.go`, ent schemas in `ent/schema/*.go`), run:
```bash
make generate
```
**Important**: Generated files must be committed to the repository. CI will fail if generated code is out of sync.
### Format Code
```bash
golangci-lint fmt
```
### Run Tests
```bash
go test -v ./...
make test
```

View File

@ -1,28 +1,45 @@
FROM --platform=$BUILDPLATFORM golang:1.23-bullseye AS build
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm@sha256:c4bc0741e3c79c0e2d47ca2505a06f5f2a44682ada94e1dba251a3854e60c2bd AS build
WORKDIR /build
# Install cross-compilation tools
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross \
&& rm -rf /var/lib/apt/lists/*
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG TARGETPLATFORM
ENV CGO_ENABLED=1
RUN make quick-vet
# Set up cross-compilation environment based on target platform
RUN case "${TARGETPLATFORM}" in \
"linux/amd64") \
CC=gcc CXX=g++ GOOS=linux GOARCH=amd64 make quick-vet ;; \
"linux/arm64") \
CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ GOOS=linux GOARCH=arm64 make quick-vet ;; \
*) echo "Unsupported platform: ${TARGETPLATFORM}" && exit 1 ;; \
esac
FROM gcr.io/distroless/cc
FROM debian:12-slim@sha256:b1a741487078b369e78119849663d7f1a5341ef2768798f7b7406c4240f86aef
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
ARG TARGETPLATFORM
LABEL org.opencontainers.image.source=https://github.com/safedep/vet
LABEL org.opencontainers.image.description="Open source software supply chain security tool"
LABEL org.opencontainers.image.licenses=Apache-2.0
LABEL io.modelcontextprotocol.server.name="io.github.safedep/vet-mcp"
COPY ./samples/ /vet/samples
COPY --from=build /build/vet /usr/local/bin/vet
USER nonroot:nonroot
ENTRYPOINT ["vet"]

View File

@ -8,20 +8,16 @@ all: quick-vet
ent:
go generate ./ent
linter-install:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.0
.PHONY: filterv2-gen
filterv2-gen:
go generate ./pkg/analyzer/filterv2/...
oapi-codegen-install:
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.10.1
generate: ent filterv2-gen
protoc-install:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
dev-setup: linter-install oapi-codegen-install protoc-install
oapi-codegen:
oapi-codegen -package insightapi -generate types ./api/insights-v1.yml > ./gen/insightapi/insights.types.go
oapi-codegen -package insightapi -generate client ./api/insights-v1.yml > ./gen/insightapi/insights.client.go
dev-setup: protoc-install
protoc-codegen:
protoc -I ./api \

694
README.md
View File

@ -1,338 +1,582 @@
<h1 align="center">
<img alt="SafeDep Vet" src="docs/static/img/vet-logo.png" width="150" />
</h1>
<div align="center">
<img width="3024" height="1964" alt="image" src="./docs/assets/vet-terminal.png" />
<p align="center">
Created and maintained by <b><a href="https://safedep.io/">https://safedep.io</a></b> with contributions from the community 🚀
<h1>SafeDep VET</h1>
<p><strong>🚀 Enterprise grade open source software supply chain security</strong></p>
<p>
<a href="https://github.com/safedep/vet/releases"><strong>Download</strong></a>
<a href="#-quick-start"><strong>Quick Start</strong></a>
<a href="https://docs.safedep.io/"><strong>Documentation</strong></a>
<a href="#-community"><strong>Community</strong></a>
</p>
</div>
<div align="center">
[![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)
![Release](https://img.shields.io/github/v/release/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)
[![CodeQL](https://github.com/safedep/vet/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/safedep/vet/actions/workflows/codeql.yml)
[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)
[![Scorecard supply-chain security](https://github.com/safedep/vet/actions/workflows/scorecard.yml/badge.svg)](https://github.com/safedep/vet/actions/workflows/scorecard.yml)
[![Twitter](https://img.shields.io/twitter/follow/safedepio?style=social)](https://twitter.com/intent/follow?screen_name=safedepio)
[![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)
[![vet banner](docs/static/img/vet/vet-banner.png)](https://safedep.io/docs)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/safedep/vet)
## Policy as Code for Open Source Software Supply Chain
</div>
`vet` is a tool for identifying risks in open source software supply chain. It
goes beyond just vulnerabilities and provides visibility on OSS package risks
due to it's license, popularity, security hygiene, and more. `vet` is designed
with the goal of helping software development teams consume safe and trusted
OSS components through automated vetting in CI/CD.
---
* [🔥 vet in action](#-vet-in-action)
* [Getting Started](#getting-started)
* [Running Scan](#running-scan)
* [Scanning Binary Artifacts](#scanning-binary-artifacts)
* [Scanning SBOM](#scanning-sbom)
* [Scanning Github Repositories](#scanning-github-repositories)
* [Scanning Github Organization](#scanning-github-organization)
* [Scanning Package URL](#scanning-package-url)
* [Available Parsers](#available-parsers)
* [Policy as Code](#policy-as-code)
* [Query Mode](#query-mode)
* [Reporting](#reporting)
* [CI/CD Integration](#ci/cd-integration)
* [📦 GitHub Action](#-github-action)
* [🚀 GitLab CI](#-gitlab-ci)
* [🐙 Malicious Package Analysis](#-malicious-package-analysis)
* [🛠️ Advanced Usage](#-advanced-usage)
* [📖 Documentation](#-documentation)
* [🎊 Community](#-community)
* [💻 Development](#-development)
* [Support](#support)
* [Star History](#star-history)
* [🔖 References](#-references)
## 🎯 Why vet?
## 🔥 vet in action
> **70-90% of modern software constitute code from open sources** — How do we know if it's safe?
![vet Demo](docs/static/img/vet/vet-demo.gif)
**vet** is an open source software supply chain security tool built for **developers and security engineers** who need:
## Getting Started
**Next-gen Software Composition Analysis** — Vulnerability and malicious package detection
**Policy as Code** — Express opinionated security policies using [CEL](https://cel.dev/)
**Real-time malicious package detection** — Powered by [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis) active scanning
**Multi-ecosystem support** — npm, PyPI, Maven, Go, Docker, GitHub Actions, and more
**CI/CD native** — Built for DevSecOps workflows with support for GitHub Actions, GitLab CI, and more
**MCP Server** — Run `vet` as a MCP server to vet open source packages from AI suggested code
**Agents** — Run AI agents to query and analyze scan results
- Download the binary file for your operating system / architecture from the [Official GitHub Releases](https://github.com/safedep/vet/releases)
## ⚡ Quick Start
- You can also install `vet` using homebrew in MacOS and Linux
**Install in seconds:**
```bash
# macOS & Linux
brew install safedep/tap/vet
```
or download a [pre-built binary](https://github.com/safedep/vet/releases)
**Scan your project:**
```bash
# Scan current directory
vet scan -D .
# Scan a single file
vet scan -M package-lock.json
# Fail CI on critical vulnerabilities
vet scan -D . --filter 'vulns.critical.exists(p, true)' --filter-fail
# Fail CI on OpenSSF Scorecard requirements
vet scan -D . --filter 'scorecard.scores.Maintained < 5' --filter-fail
# Fail CI if a package is published from a GitHub repository with less than 5 stars
vet scan -D . --filter 'projects.exists(p, p.type == "GITHUB" && p.stars < 5)' --filter-fail
```
## 🔒 Key Features
### 🕵️ **Code Analysis**
Unlike dependency scanners that flood you with noise, `vet` analyzes your **actual code usage** to prioritize real risks. See [dependency usage evidence](https://docs.safedep.io/guides/dependency-usage-identification) for more details.
### 🛡️ **Malicious Package Detection**
Integrated with [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis) for real-time protection against malicious packages in the wild. Free for open source projects. Fallback to _Query Mode_ when API key is not provided. Read more [about malicious package scanning](#-malicious-package-detection-1).
### 📋 **Policy as Code**
Define security policies using CEL expressions to enforce context specific security requirements.
```bash
# Block packages with critical CVEs
vet scan \
--filter 'vulns.critical.exists(p, true)'
# Enforce license compliance
vet scan \
--filter 'licenses.contains_license("GPL-3.0")'
# Enforce OpenSSF Scorecard requirements
# Require minimum OpenSSF Scorecard scores
vet scan \
--filter 'scorecard.scores.Maintained < 5'
```
### 🎯 **Multi-Format Support**
- **Package Managers**: npm, PyPI, Maven, Go, Ruby, Rust, PHP
- **Container Images**: Docker, OCI
- **SBOMs**: CycloneDX, SPDX
- **Binary Artifacts**: JAR files, Python wheels
- **Source Code**: Direct repository scanning
## 🔥 See vet in Action
<div align="center">
<img src="./docs/assets/vet-demo.gif" alt="vet Demo" width="100%" />
</div>
## 🚀 Production Ready Integrations
### 📦 **GitHub Actions**
Zero config security guardrails against vulnerabilities and malicious packages in your CI/CD pipeline
**with your own opinionated policies**:
```yaml
- uses: safedep/vet-action@v1
with:
policy: ".github/vet/policy.yml"
```
See more in [vet-action](https://github.com/safedep/vet-action) documentation.
### 🔧 **GitLab CI**
Enterprise grade scanning with [vet CI Component](https://gitlab.com/explore/catalog/safedep/ci-components/vet):
```yaml
include:
- component: gitlab.com/safedep/ci-components/vet/scan@main
```
### 🐳 **Container Integration**
Run `vet` anywhere, even your internal developer platform or custom CI/CD environment using our container image.
```bash
docker run --rm -v $(pwd):/app ghcr.io/safedep/vet:latest scan -D /app
```
## 📚 Table of Contents
- [🎯 Why vet?](#-why-vet)
- [⚡ Quick Start](#-quick-start)
- [🔒 Key Features](#-key-features)
- [🕵️ **Code Analysis**](#-code-analysis)
- [🛡️ **Malicious Package Detection**](#-malicious-package-detection)
- [📋 **Policy as Code**](#-policy-as-code)
- [🎯 **Multi-Format Support**](#-multi-format-support)
- [🔥 See vet in Action](#-see-vet-in-action)
- [🚀 Production Ready Integrations](#-production-ready-integrations)
- [📦 **GitHub Actions**](#-github-actions)
- [🔧 **GitLab CI**](#-gitlab-ci)
- [🐳 **Container Integration**](#-container-integration)
- [📚 Table of Contents](#-table-of-contents)
- [📦 Installation Options](#-installation-options)
- [🍺 **Homebrew (Recommended)**](#-homebrew-recommended)
- [📥 **Direct Download**](#-direct-download)
- [🐹 **Go Install**](#-go-install)
- [🐳 **Container Image**](#-container-image)
- [⚙️ **Verify Installation**](#-verify-installation)
- [🎮 Advanced Usage](#-advanced-usage)
- [🔍 **Scanning Options**](#-scanning-options)
- [🎯 **Policy Enforcement Examples**](#-policy-enforcement-examples)
- [🔧 **SBOM Support**](#-sbom-support)
- [📊 **Query Mode \& Data Persistence**](#-query-mode--data-persistence)
- [📊 Reporting](#-reporting)
- [📋 **Report Formats**](#-report-formats)
- [🎯 **Report Examples**](#-report-examples)
- [🤖 **MCP Server**](#-mcp-server)
- [🤖 **Agents**](#-agents)
- [🛡️ Malicious Package Detection](#-malicious-package-detection-1)
- [🚀 **Quick Setup**](#-quick-setup)
- [🎯 **Advanced Malicious Package Analysis**](#-advanced-malicious-package-analysis)
- [🔒 **Security Features**](#-security-features)
- [📊 Privacy and Telemetry](#-privacy-and-telemetry)
- [🎊 Community \& Support](#-community--support)
- [🌟 **Join the Community**](#-join-the-community)
- [💡 **Get Help \& Share Ideas**](#-get-help--share-ideas)
- [⭐ **Star History**](#-star-history)
- [🙏 **Built With Open Source**](#-built-with-open-source)
## 📦 Installation Options
### 🍺 **Homebrew (Recommended)**
```bash
brew tap safedep/tap
brew install safedep/tap/vet
```
- Alternatively, build from source
### 📥 **Direct Download**
> Ensure $(go env GOPATH)/bin is in your $PATH
See [releases](https://github.com/safedep/vet/releases) for the latest version.
### 🐹 **Go Install**
```bash
go install github.com/safedep/vet@latest
```
- Also available as a container image
### 🐳 **Container Image**
```bash
docker run --rm -it ghcr.io/safedep/vet:latest version
# Quick test
docker run --rm ghcr.io/safedep/vet:latest version
# Scan local directory
docker run --rm -v $(pwd):/workspace ghcr.io/safedep/vet:latest scan -D /workspace
```
> **Note:** Container image is built for x86_64 Linux only. Use a
> [pre-built binary](https://github.com/safedep/vet/releases) or
> build from source for other platforms.
### Running Scan
- Run `vet` to identify risks by scanning a directory
### ⚙️ **Verify Installation**
```bash
vet scan -D /path/to/repository
vet version
# Should display version and build information
```
![vet scan directory](docs/static/img/vet/vet-scan-directory.png)
## 🎮 Advanced Usage
- Run `vet` to scan specific (supported) package manifests
### 🔍 **Scanning Options**
<table>
<tr>
<td width="50%">
**📁 Directory Scanning**
```bash
vet scan -M /path/to/pom.xml
vet scan -M /path/to/requirements.txt
vet scan -M /path/to/package-lock.json
# Scan current directory
vet scan
# Scan a given directory
vet scan -D /path/to/project
# Resolve and scan transitive dependencies
vet scan -D . --transitive
```
**Note:** `--lockfiles` is generalized to `-M` or `--manifests` to support additional
types of package manifests or other artifacts in future.
#### Scanning Binary Artifacts
- Scan a Java JAR file
**📄 Manifest Files**
```bash
vet scan -M /path/to/app.jar
# Package managers
vet scan -M package-lock.json
vet scan -M requirements.txt
vet scan -M pom.xml
vet scan -M go.mod
vet scan -M Gemfile.lock
```
> Suitable for scanning bootable JARs with embedded dependencies
</td>
<td width="50%">
- Scan a directory with JAR files
```bash
vet scan -D /path/to/jars --type jar
```
#### Scanning SBOM
- Scan an SBOM in [CycloneDX](https://cyclonedx.org/) format
```bash
vet scan -M /path/to/cyclonedx-sbom.json --type bom-cyclonedx
```
- Scan an SBOM in [SPDX](https://spdx.dev/) format
```bash
vet scan -M /path/to/spdx-sbom.json --type bom-spdx
```
**Note:** `--type` is a generalized version of `--lockfile-as` to support additional
artifact types in future.
> **Note:** SBOM scanning feature is currently in experimental stage
#### Scanning Github Repositories
- Setup github access token to scan private repo
**🐙 GitHub Integration**
```bash
# Setup GitHub access
vet connect github
# Scan repositories
vet scan --github https://github.com/user/repo
# Organization scanning
vet scan --github-org https://github.com/org
```
Alternatively, set `GITHUB_TOKEN` environment variable with [Github PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)
- To scan remote Github repositories, including private ones
**📦 Artifact Scanning**
```bash
vet scan --github https://github.com/safedep/vet
# Container images
vet scan --image nginx:latest
vet scan --image /path/to/image-saved-file.tar
# Binary artifacts
vet scan -M app.jar
vet scan -M package.whl
```
**Note:** You may need to enable [Dependency Graph](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-the-dependency-graph) at repository or organization level for Github repository scanning to work.
</td>
</tr>
</table>
#### Scanning Github Organization
> You must setup the required access for scanning private repositories
> before scanning organizations
### 🎯 **Policy Enforcement Examples**
```bash
vet scan --github-org https://github.com/safedep
```
> **Note:** `vet` will block and wait if it encounters Github secondary rate limit.
#### Scanning Package URL
- To scan a [purl](https://github.com/package-url/purl-spec)
```bash
vet scan --purl pkg:/gem/nokogiri@1.10.4
```
#### Available Parsers
- List supported package manifest parsers including experimental modules
```bash
vet scan parsers --experimental
```
## Policy as Code
`vet` uses [Common Expressions Language](https://github.com/google/cel-spec)
(CEL) as the policy language. Policies can be defined to build guardrails
preventing introduction of insecure components.
### Vulnerability
- Run `vet` and fail if a critical or high vulnerability was detected
```bash
vet scan -D /path/to/code \
# Security-first scanning
vet scan -D . \
--filter 'vulns.critical.exists(p, true) || vulns.high.exists(p, true)' \
--filter-fail
```
### License
# License compliance
vet scan -D . \
--filter 'licenses.contains_license("GPL-3.0")' \
--filter-fail
- Run `vet` and fail if a package with a specific license was detected
# OpenSSF Scorecard requirements
vet scan -D . \
--filter 'scorecard.scores.Maintained < 5' \
--filter-fail
```bash
vet scan -D /path/to/code \
--filter 'licenses.exists(p, "GPL-2.0")' \
# Popularity-based filtering
vet scan -D . \
--filter 'projects.exists(p, p.type == "GITHUB" && p.stars < 50)' \
--filter-fail
```
**Note:** Using `licenses.contains_license(...)` is recommended for license matching due
to its support for SPDX expressions.
- `vet` supports [SPDX License Expressions](https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/) at package license and policy level
### 🔧 **SBOM Support**
```bash
vet scan -D /path/to/code \
--filter 'licenses.contains_license("LGPL-2.1+")' \
--filter-fail
# Scan a CycloneDX SBOM
vet scan -M sbom.json --type bom-cyclonedx
# Scan a SPDX SBOM
vet scan -M sbom.spdx.json --type bom-spdx
# Generate SBOM output
vet scan -D . --report-cdx=output.sbom.json
# Package URL scanning
vet scan --purl pkg:npm/lodash@4.17.21
```
### Scorecard
### 📊 **Query Mode & Data Persistence**
- Run `vet` and fail based on [OpenSSF Scorecard](https://securityscorecards.dev/) attributes
For large codebases and repeated analysis:
```bash
vet scan -D /path/to/code \
--filter 'scorecard.scores.Maintained == 0' \
--filter-fail
# Scan once, query multiple times
vet scan -D . --json-dump-dir ./scan-data
# Query with different filters
vet query --from ./scan-data \
--filter 'vulns.critical.exists(p, true)'
# Generate focused reports
vet query --from ./scan-data \
--filter 'licenses.contains_license("GPL")' \
--report-json license-violations.json
```
For more examples, refer to [documentation](https://docs.safedep.io/advanced/policy-as-code)
## 📊 Reporting
## Query Mode
**vet** generate reports that are tailored for different stakeholders:
- Run scan and dump internal data structures to a file for further querying
### 📋 **Report Formats**
<table>
<tr>
<td width="30%"><strong>🔍 For Security Teams</strong></td>
<td width="70%">
```bash
vet scan -D /path/to/code --json-dump-dir /path/to/dump
# SARIF for GitHub Security tab
vet scan -D . --report-sarif=report.sarif
# JSON for custom tooling
vet scan -D . --report-json=report.json
# CSV for spreadsheet analysis
vet scan -D . --report-csv=report.csv
# HTML for web-based analysis
vet scan -D . --report-html=report.html
```
- Filter results using `query` command
</td>
</tr>
<tr>
<td><strong>📖 For Developers</strong></td>
<td>
```bash
vet query --from /path/to/dump \
--filter 'vulns.critical.exists(p, true) || vulns.high.exists(p, true)'
# Markdown reports for PRs
vet scan -D . --report-markdown=report.md
# Console summary (default)
vet scan -D . --report-summary
```
- Generate report from dumped data
</td>
</tr>
<tr>
<td><strong>🏢 For Compliance</strong></td>
<td>
```bash
vet query --from /path/to/dump --report-json /path/to/report.json
# SBOM generation
vet scan -D . --report-cdx=sbom.json
# Dependency graphs
vet scan -D . --report-graph=dependencies.dot
```
## Reporting
</td>
</tr>
</table>
`vet` supports generating reports in multiple formats during `scan` or `query`
execution.
| Format | Description |
|----------|--------------------------------------------------------------------------------|
| Markdown | Human readable report for vulnerabilities, licenses, and more |
| CSV | Export data to CSV format for manual slicing and dicing |
| JSON | Machine readable JSON format following internal schema (maximum data) |
| SARIF | Useful for integration with Github Code Scanning and other tools |
| Graph | Dependency graph in DOT format for risk and package relationship visualization |
| Summary | Default console report with summary of vulnerabilities, licenses, and more |
## CI/CD Integration
### 📦 GitHub Action
- `vet` is available as a GitHub Action, refer to [vet-action](https://github.com/safedep/vet-action)
### 🚀 GitLab CI
- `vet` can be integrated with GitLab CI, refer to [vet-gitlab-ci](https://docs.safedep.io/integrations/gitlab-ci)
## 🐙 Malicious Package Analysis
`vet` supports scanning for malicious packages using [SafeDep Cloud API](https://docs.safedep.io/cloud/malware-analysis)
- Run a scan and check for malicious packages
### 🎯 **Report Examples**
```bash
vet scan -D /path/to/code --malware
# Multi-format output
vet scan -D . \
--report-json=report.json \
--report-sarif=report.sarif \
--report-markdown=report.md \
--report-html=report.html
# Focus on specific issues
vet scan -D . \
--filter 'vulns.high.exists(p, true)' \
--report-json=report.json
```
**Note**: `vet` will submit identified packages to SafeDep Cloud for analysis and wait
for a `timeout` period for response. Not all package analysis may be completed
within the timeout period. However, subsequent scans will fetch the results if
available and lead to increased coverage over time. Adjust the timeout using
`--malware-analysis-timeout` flag.
### 🤖 **MCP Server**
## 🛠️ Advanced Usage
**vet** can be used as an MCP server to vet open source packages from AI suggested code.
- [Threat Hunting with vet](https://docs.safedep.io/advanced/filtering)
- [Policy as Code](https://docs.safedep.io/advanced/policy-as-code)
- [Exceptions and Overrides](https://docs.safedep.io/advanced/exceptions)
```bash
# Start the MCP server with SSE transport
vet server mcp --server-type sse
```
## 📖 Documentation
For more details, see [vet MCP Server](./docs/mcp.md) documentation.
- Refer to [https://safedep.io/docs](https://safedep.io/docs) for the detailed documentation
### 🤖 **Agents**
[![vet docs](docs/static/img/vet-docs.png)](https://safedep.io/docs)
See [vet Agents](./docs/agent.md) documentation for more details.
## 🎊 Community
## 🛡️ Malicious Package Detection
First of all, thank you so much for showing interest in `vet`, we appreciate it ❤️
**Malicious package detection through active scanning and code analysis** powered by
[SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis). `vet` requires an API
key for active scanning of unknown packages. When API key is not provided, `vet` will
fallback to _Query Mode_ which detects known malicious packages from [SafeDep](https://safedep.io)
and [OSV](https://osv.dev) databases.
- Join the Discord server using the link - [https://rebrand.ly/safedep-community](https://rebrand.ly/safedep-community)
- Grab a free API key by running `vet cloud quickstart`
- API access is free forever for open source projects
- No proprietary code is collected for malicious package detection
- Only open source package scanning from public repositories is supported
[![SafeDep Discord](docs/static/img/safedep-discord.png)](https://rebrand.ly/safedep-community)
### 🚀 **Quick Setup**
## 💻 Development
> Malicious package detection requires an API key for [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis).
Refer to [CONTRIBUTING.md](CONTRIBUTING.md)
```bash
# One-time setup
vet cloud quickstart
## Support
# Enable malware scanning
vet scan -D . --malware
[SafeDep](https://safedep.io) provides enterprise support for `vet`
deployments. Check out [SafeDep Cloud](https://safedep.io) for large scale
deployment and management of `vet` in your organization.
# Query for known malicious packages without API key
vet scan -D . --malware-query
```
## Star History
Example malicious packages detected and reported by [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis)
malicious package detection:
- [MAL-2025-3541: express-cookie-parser](https://safedep.io/malicious-npm-package-express-cookie-parser/)
- [MAL-2025-4339: eslint-config-airbnb-compat](https://safedep.io/digging-into-dynamic-malware-analysis-signals/)
- [MAL-2025-4029: ts-runtime-compat-check](https://safedep.io/digging-into-dynamic-malware-analysis-signals/)
- [MAL-2025-2227: nyc-config](https://safedep.io/nyc-config-malicious-package/)
### 🎯 **Advanced Malicious Package Analysis**
<table>
<tr>
<td width="50%">
**🔍 Scan packages with malicious package detection enabled**
```bash
# Real-time scanning
vet scan -D . --malware
# Timeout adjustment
vet scan -D . --malware \
--malware-analysis-timeout=300s
# Batch analysis
vet scan -D . --malware \
--json-dump-dir=./analysis
```
</td>
<td width="50%">
**🎭 Specialized Scans**
```bash
# VS Code extensions
vet scan --vsx --malware
# GitHub Actions
vet scan -D .github/workflows --malware
# Container Images
vet scan --image nats:2.10 --malware
# Scan a single package and fail if its malicious
vet scan --purl pkg:/npm/nyc-config@10.0.0 --fail-fast
# Active scanning of a single package (requires API key)
vet inspect malware \
--purl pkg:npm/nyc-config@10.0.0
```
</td>
</tr>
</table>
### 🔒 **Security Features**
- ✅ **Real-time analysis** of packages against known malware databases
- ✅ **Behavioral analysis** using static and dynamic analysis
- ✅ **Zero day protection** through active code scanning
- ✅ **Human in the loop** for triaging and investigation of high impact findings
- ✅ **Real time analysis** with public [analysis log](https://vetpkg.dev/mal)
## 📊 Privacy and Telemetry
`vet` collects anonymous usage telemetry to improve the product. **Your code and package information is never transmitted.**
```bash
# Disable telemetry (optional)
export VET_DISABLE_TELEMETRY=true
```
## 🎊 Community & Support
<div align="center">
### 🌟 **Join the Community**
[![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)
## 🔖 References
### 🙏 **Built With Open Source**
- https://github.com/google/osv-scanner
- https://github.com/anchore/syft
- https://deps.dev/
- https://securityscorecards.dev/
- https://slsa.dev/
vet stands on the shoulders of giants:
[OSV](https://osv.dev) • [OpenSSF Scorecard](https://securityscorecards.dev/) • [SLSA](https://slsa.dev/) • [OSV-SCALIBR](https://github.com/google/osv-scalibr) • [Syft](https://github.com/anchore/syft)
---
<p><strong>⚡ Secure your supply chain today. Star the repo ⭐ and get started!</strong></p>
Created with ❤️ by [SafeDep](https://safedep.io) and the open source community
</div>
<img referrerpolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=304d1856-fcb3-4166-bfbf-b3e40d0f1e3b" />

69
agent/agent.go Normal file
View File

@ -0,0 +1,69 @@
// Package agent declares the building blocks for implement vet agent.
package agent
import (
"context"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
)
type Input struct {
Query string
}
type AnswerFormat string
const (
AnswerFormatMarkdown AnswerFormat = "markdown"
AnswerFormatJSON AnswerFormat = "json"
)
type Output struct {
Answer string
Format AnswerFormat
}
type Memory interface {
AddInteraction(ctx context.Context, interaction *schema.Message) error
GetInteractions(ctx context.Context) ([]*schema.Message, error)
Clear(ctx context.Context) error
}
type Session interface {
ID() string
Memory() Memory
}
// AgentExecutionContext is to pass additional context to the agent
// on a per execution basis. This is required so that an agent can be configured
// and shared with different components while allowing the component to pass
// additional context to the agent.
type AgentExecutionContext struct {
// OnToolCall is called when the agent is about to call a tool.
// This is used for introspection only and not to mutate the agent's behavior.
OnToolCall func(context.Context, Session, Input, string, string) error
}
type AgentExecutionContextOpt func(*AgentExecutionContext)
func WithToolCallHook(fn func(context.Context, Session, Input, string, string) error) AgentExecutionContextOpt {
return func(a *AgentExecutionContext) {
a.OnToolCall = fn
}
}
type Agent interface {
// Execute executes the agent with the given input and returns the output.
// Internally the agent may perform a multi-step operation based on config,
// instructions and available tools.
Execute(context.Context, Session, Input, ...AgentExecutionContextOpt) (Output, error)
}
// AgentToolCallIntrospectionFn is a function that introspects a tool call.
// This is aligned with eino contract.
type AgentToolCallIntrospectionFn func(context.Context /* name */, string /* args */, string) ( /* args */ string, error)
type ToolBuilder interface {
Build(context.Context) ([]tool.BaseTool, error)
}

165
agent/llm.go Normal file
View File

@ -0,0 +1,165 @@
package agent
import (
"context"
"fmt"
"os"
"github.com/cloudwego/eino-ext/components/model/claude"
"github.com/cloudwego/eino-ext/components/model/gemini"
"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/components/model"
"google.golang.org/genai"
)
// Map of fast vs. default models.
var defaultModelMap = map[string]map[string]string{
"openai": {
"default": "gpt-4o",
"fast": "gpt-4o-mini",
},
"claude": {
"default": "claude-sonnet-4-20250514",
"fast": "claude-sonnet-4-20250514",
},
"gemini": {
"default": "gemini-2.5-pro",
"fast": "gemini-2.5-flash",
},
}
type Model struct {
Vendor string
Name string
Fast bool
Client model.ToolCallingChatModel
}
// BuildModelFromEnvironment builds a model from the environment variables.
// The order of preference is:
// 1. OpenAI
// 2. Claude
// 3. Gemini
// 4. Others..
func BuildModelFromEnvironment(fastMode bool) (*Model, error) {
if model, err := buildOpenAIModelFromEnvironment(fastMode); err == nil {
return model, nil
}
if model, err := buildClaudeModelFromEnvironment(fastMode); err == nil {
return model, nil
}
if model, err := buildGeminiModelFromEnvironment(fastMode); err == nil {
return model, nil
}
return nil, fmt.Errorf("no usable LLM found for use with agent")
}
func buildOpenAIModelFromEnvironment(fastMode bool) (*Model, error) {
defaultModel := defaultModelMap["openai"]["default"]
if fastMode {
defaultModel = defaultModelMap["openai"]["fast"]
}
modelName := os.Getenv("OPENAI_MODEL_OVERRIDE")
if modelName == "" {
modelName = defaultModel
}
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("OPENAI_API_KEY is not set")
}
model, err := openai.NewChatModel(context.Background(), &openai.ChatModelConfig{
Model: modelName,
APIKey: apiKey,
})
if err != nil {
return nil, fmt.Errorf("failed to create openai model: %w", err)
}
return &Model{
Vendor: "openai",
Name: modelName,
Fast: fastMode,
Client: model,
}, nil
}
func buildClaudeModelFromEnvironment(fastMode bool) (*Model, error) {
defaultModel := defaultModelMap["claude"]["default"]
if fastMode {
defaultModel = defaultModelMap["claude"]["fast"]
}
modelName := os.Getenv("ANTHROPIC_MODEL_OVERRIDE")
if modelName == "" {
modelName = defaultModel
}
apiKey := os.Getenv("ANTHROPIC_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("ANTHROPIC_API_KEY is not set")
}
model, err := claude.NewChatModel(context.Background(), &claude.Config{
Model: modelName,
APIKey: apiKey,
})
if err != nil {
return nil, fmt.Errorf("failed to create claude model: %w", err)
}
return &Model{
Vendor: "claude",
Name: modelName,
Fast: fastMode,
Client: model,
}, nil
}
func buildGeminiModelFromEnvironment(fastMode bool) (*Model, error) {
defaultModel := defaultModelMap["gemini"]["default"]
if fastMode {
defaultModel = defaultModelMap["gemini"]["fast"]
}
modelName := os.Getenv("GEMINI_MODEL_OVERRIDE")
if modelName == "" {
modelName = defaultModel
}
apiKey := os.Getenv("GEMINI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("GEMINI_API_KEY is not set")
}
client, err := genai.NewClient(context.Background(), &genai.ClientConfig{
APIKey: apiKey,
})
if err != nil {
return nil, fmt.Errorf("failed to create gemini client: %w", err)
}
model, err := gemini.NewChatModel(context.Background(), &gemini.Config{
Model: modelName,
Client: client,
ThinkingConfig: &genai.ThinkingConfig{
IncludeThoughts: false,
ThinkingBudget: nil,
},
})
if err != nil {
return nil, fmt.Errorf("failed to create gemini model: %w", err)
}
return &Model{
Vendor: "gemini",
Name: modelName,
Fast: fastMode,
Client: model,
}, nil
}

17
agent/llm_test.go Normal file
View File

@ -0,0 +1,17 @@
package agent
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDefaultModelsMap(t *testing.T) {
t.Run("default model map must have vendor, model, and fast model", func(t *testing.T) {
for vendor, models := range defaultModelMap {
assert.NotEmpty(t, vendor)
assert.NotEmpty(t, models["default"])
assert.NotEmpty(t, models["fast"])
}
})
}

145
agent/mcp.go Normal file
View File

@ -0,0 +1,145 @@
package agent
import (
"context"
"fmt"
"os"
"path/filepath"
einomcp "github.com/cloudwego/eino-ext/components/tool/mcp"
"github.com/cloudwego/eino/components/tool"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)
type McpClientToolBuilderConfig struct {
// Common config
ClientName string
ClientVersion string
// SSE client config
SseURL string
Headers map[string]string
// Stdout client config
SkipDefaultTools bool
SQLQueryToolEnabled bool
SQLQueryToolDBPath string
PackageRegistryToolEnabled bool
// Enable debug mode for the MCP client.
Debug bool
}
type mcpClientToolBuilder struct {
config McpClientToolBuilderConfig
}
var _ ToolBuilder = (*mcpClientToolBuilder)(nil)
// NewMcpClientToolBuilder creates a new MCP client tool builder for `vet` MCP server.
// This basically connects to vet MCP server over SSE or executes the `vet server mcp` command
// to start a MCP server in stdio mode. We maintain loose coupling between the MCP client and the MCP server
// by allowing the client to be configured with a set of flags to enable/disable specific tools. We do this
// to ensure vet MCP contract is not violated and evolves independently. vet Agents will in turn depend on
// vet MCP server for data access.
func NewMcpClientToolBuilder(config McpClientToolBuilderConfig) (*mcpClientToolBuilder, error) {
return &mcpClientToolBuilder{
config: config,
}, nil
}
func (b *mcpClientToolBuilder) Build(ctx context.Context) ([]tool.BaseTool, error) {
var cli *client.Client
var err error
if b.config.SseURL != "" {
cli, err = b.buildSseClient()
if err != nil {
return nil, fmt.Errorf("failed to create sse client: %w", err)
}
} else {
cli, err = b.buildStdioClient()
if err != nil {
return nil, fmt.Errorf("failed to create stdio client: %w", err)
}
}
err = cli.Start(ctx)
if err != nil {
return nil, fmt.Errorf("failed to start mcp client: %w", err)
}
initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: b.config.ClientName,
Version: b.config.ClientVersion,
}
_, err = cli.Initialize(ctx, initRequest)
if err != nil {
return nil, fmt.Errorf("failed to initialize mcp client: %w", err)
}
tools, err := einomcp.GetTools(ctx, &einomcp.Config{
Cli: cli,
})
if err != nil {
return nil, fmt.Errorf("failed to get tools: %w", err)
}
return tools, nil
}
func (b *mcpClientToolBuilder) buildSseClient() (*client.Client, error) {
cli, err := client.NewSSEMCPClient(b.config.SseURL, client.WithHeaders(b.config.Headers))
if err != nil {
return nil, fmt.Errorf("failed to create sse client: %w", err)
}
return cli, nil
}
// buildStdioClient is used to start vet mcp server with arguments
// based on the configuration.
func (b *mcpClientToolBuilder) buildStdioClient() (*client.Client, error) {
binaryPath, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("failed to get running binary path: %w", err)
}
// vet-mcp server defaults to stdio transport. See cmd/server/mcp.go
vetMcpServerCommandArgs := []string{"server", "mcp"}
if b.config.Debug {
vetMcpServerLogFile := filepath.Join(os.TempDir(), "vet-mcp-server.log")
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "-l", vetMcpServerLogFile)
}
if b.config.SQLQueryToolEnabled {
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "--sql-query-tool")
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "--sql-query-tool-db-path",
b.config.SQLQueryToolDBPath)
}
if b.config.PackageRegistryToolEnabled {
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "--package-registry-tool")
}
if b.config.SkipDefaultTools {
vetMcpServerCommandArgs = append(vetMcpServerCommandArgs, "--skip-default-tools")
}
environmentVariables := []string{}
if b.config.Debug {
environmentVariables = append(environmentVariables, "APP_LOG_LEVEL=debug")
}
cli, err := client.NewStdioMCPClient(binaryPath, environmentVariables, vetMcpServerCommandArgs...)
if err != nil {
return nil, fmt.Errorf("failed to create stdio client: %w", err)
}
return cli, nil
}

45
agent/memory.go Normal file
View File

@ -0,0 +1,45 @@
package agent
import (
"context"
"sync"
"github.com/cloudwego/eino/schema"
)
type simpleMemory struct {
mutex sync.RWMutex
interactions []*schema.Message
}
var _ Memory = (*simpleMemory)(nil)
func NewSimpleMemory() (*simpleMemory, error) {
return &simpleMemory{
interactions: make([]*schema.Message, 0),
}, nil
}
func (m *simpleMemory) AddInteraction(ctx context.Context, interaction *schema.Message) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.interactions = append(m.interactions, interaction)
return nil
}
func (m *simpleMemory) GetInteractions(ctx context.Context) ([]*schema.Message, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.interactions, nil
}
func (m *simpleMemory) Clear(ctx context.Context) error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.interactions = make([]*schema.Message, 0)
return nil
}

279
agent/memory_test.go Normal file
View File

@ -0,0 +1,279 @@
package agent
import (
"context"
"fmt"
"sync"
"testing"
"github.com/cloudwego/eino/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewSimpleMemory(t *testing.T) {
memory, err := NewSimpleMemory()
require.NoError(t, err)
assert.NotNil(t, memory)
// Test that initial interactions are empty
ctx := context.Background()
interactions, err := memory.GetInteractions(ctx)
require.NoError(t, err)
assert.Equal(t, 0, len(interactions))
}
func TestSimpleMemory_AddInteraction(t *testing.T) {
memory, err := NewSimpleMemory()
require.NoError(t, err)
ctx := context.Background()
message := &schema.Message{
Role: schema.User,
Content: "test message",
}
err = memory.AddInteraction(ctx, message)
assert.NoError(t, err)
interactions, err := memory.GetInteractions(ctx)
require.NoError(t, err)
assert.Equal(t, 1, len(interactions))
assert.Equal(t, message, interactions[0])
}
func TestSimpleMemory_AddMultipleInteractions(t *testing.T) {
memory, err := NewSimpleMemory()
require.NoError(t, err)
ctx := context.Background()
messages := []*schema.Message{
{Role: schema.User, Content: "first message"},
{Role: schema.Assistant, Content: "second message"},
{Role: schema.User, Content: "third message"},
}
for _, msg := range messages {
err = memory.AddInteraction(ctx, msg)
assert.NoError(t, err)
}
interactions, err := memory.GetInteractions(ctx)
require.NoError(t, err)
assert.Equal(t, 3, len(interactions))
for i, msg := range messages {
assert.Equal(t, msg, interactions[i])
}
}
func TestSimpleMemory_GetInteractions(t *testing.T) {
memory, err := NewSimpleMemory()
require.NoError(t, err)
ctx := context.Background()
// Test empty interactions
interactions, err := memory.GetInteractions(ctx)
assert.NoError(t, err)
assert.NotNil(t, interactions)
assert.Equal(t, 0, len(interactions))
// Add some interactions
message1 := &schema.Message{Role: schema.User, Content: "message 1"}
message2 := &schema.Message{Role: schema.Assistant, Content: "message 2"}
err = memory.AddInteraction(ctx, message1)
require.NoError(t, err)
err = memory.AddInteraction(ctx, message2)
require.NoError(t, err)
interactions, err = memory.GetInteractions(ctx)
assert.NoError(t, err)
assert.Equal(t, 2, len(interactions))
assert.Equal(t, message1, interactions[0])
assert.Equal(t, message2, interactions[1])
}
func TestSimpleMemory_Clear(t *testing.T) {
memory, err := NewSimpleMemory()
require.NoError(t, err)
ctx := context.Background()
// Add some interactions
message1 := &schema.Message{Role: schema.User, Content: "message 1"}
message2 := &schema.Message{Role: schema.Assistant, Content: "message 2"}
err = memory.AddInteraction(ctx, message1)
require.NoError(t, err)
err = memory.AddInteraction(ctx, message2)
require.NoError(t, err)
// Verify interactions exist
interactions, err := memory.GetInteractions(ctx)
require.NoError(t, err)
assert.Equal(t, 2, len(interactions))
// Clear interactions
err = memory.Clear(ctx)
assert.NoError(t, err)
// Verify interactions are cleared
interactions, err = memory.GetInteractions(ctx)
assert.NoError(t, err)
assert.Equal(t, 0, len(interactions))
}
func TestSimpleMemory_NilInteraction(t *testing.T) {
memory, err := NewSimpleMemory()
require.NoError(t, err)
ctx := context.Background()
// Test adding nil interaction
err = memory.AddInteraction(ctx, nil)
assert.NoError(t, err)
interactions, err := memory.GetInteractions(ctx)
require.NoError(t, err)
assert.Equal(t, 1, len(interactions))
assert.Nil(t, interactions[0])
}
func TestSimpleMemory_ConcurrentAccess(t *testing.T) {
memory, err := NewSimpleMemory()
require.NoError(t, err)
ctx := context.Background()
numGoroutines := 100
messagesPerGoroutine := 10
var wg sync.WaitGroup
wg.Add(numGoroutines)
// Concurrent writes
for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) {
defer wg.Done()
for j := 0; j < messagesPerGoroutine; j++ {
message := &schema.Message{
Role: schema.User,
Content: fmt.Sprintf("goroutine-%d-message-%d", goroutineID, j),
}
err := memory.AddInteraction(ctx, message)
assert.NoError(t, err)
}
}(i)
}
wg.Wait()
// Verify all interactions were added
interactions, err := memory.GetInteractions(ctx)
require.NoError(t, err)
assert.Equal(t, numGoroutines*messagesPerGoroutine, len(interactions))
}
func TestSimpleMemory_ConcurrentReadWrite(t *testing.T) {
memory, err := NewSimpleMemory()
require.NoError(t, err)
ctx := context.Background()
numReaders := 10
numWriters := 10
messagesPerWriter := 5
var wg sync.WaitGroup
wg.Add(numReaders + numWriters)
// Concurrent writers
for i := 0; i < numWriters; i++ {
go func(writerID int) {
defer wg.Done()
for j := 0; j < messagesPerWriter; j++ {
message := &schema.Message{
Role: schema.User,
Content: fmt.Sprintf("writer-%d-message-%d", writerID, j),
}
err := memory.AddInteraction(ctx, message)
assert.NoError(t, err)
}
}(i)
}
// Concurrent readers
for i := 0; i < numReaders; i++ {
go func() {
defer wg.Done()
for j := 0; j < messagesPerWriter; j++ {
interactions, err := memory.GetInteractions(ctx)
assert.NoError(t, err)
assert.NotNil(t, interactions)
// Length can vary due to concurrent writes
assert.GreaterOrEqual(t, len(interactions), 0)
}
}()
}
wg.Wait()
// Final verification
interactions, err := memory.GetInteractions(ctx)
require.NoError(t, err)
assert.Equal(t, numWriters*messagesPerWriter, len(interactions))
}
func TestSimpleMemory_ClearDuringConcurrentAccess(t *testing.T) {
memory, err := NewSimpleMemory()
require.NoError(t, err)
ctx := context.Background()
numWriters := 5
messagesPerWriter := 10
var wg sync.WaitGroup
wg.Add(numWriters + 1) // +1 for the clearer
// Add some initial interactions
for i := 0; i < 5; i++ {
message := &schema.Message{
Role: schema.User,
Content: fmt.Sprintf("initial-message-%d", i),
}
err := memory.AddInteraction(ctx, message)
require.NoError(t, err)
}
// Concurrent writers
for i := 0; i < numWriters; i++ {
go func(writerID int) {
defer wg.Done()
for j := 0; j < messagesPerWriter; j++ {
message := &schema.Message{
Role: schema.User,
Content: fmt.Sprintf("writer-%d-message-%d", writerID, j),
}
err := memory.AddInteraction(ctx, message)
assert.NoError(t, err)
}
}(i)
}
// Clear operation
go func() {
defer wg.Done()
// Clear after some writes have happened
err := memory.Clear(ctx)
assert.NoError(t, err)
}()
wg.Wait()
// Final state check - should be consistent
interactions, err := memory.GetInteractions(ctx)
require.NoError(t, err)
assert.NotNil(t, interactions)
// The exact number depends on timing of clear operation
assert.GreaterOrEqual(t, len(interactions), 0)
}

156
agent/mock.go Normal file
View File

@ -0,0 +1,156 @@
package agent
import (
"context"
"fmt"
"strings"
"github.com/cloudwego/eino/schema"
)
// MockAgent provides a simple implementation of the Agent interface for testing
type mockAgent struct{}
// MockSession is a simple session implementation
type mockSession struct {
sessionID string
memory Memory
}
type mockMemory struct {
interactions []*schema.Message
}
func (m *mockMemory) AddInteraction(ctx context.Context, interaction *schema.Message) error {
m.interactions = append(m.interactions, interaction)
return nil
}
func (m *mockMemory) GetInteractions(ctx context.Context) ([]*schema.Message, error) {
return m.interactions, nil
}
func (m *mockMemory) Clear(ctx context.Context) error {
m.interactions = make([]*schema.Message, 0)
return nil
}
// NewMockAgent creates a new mock agent
func NewMockAgent() *mockAgent {
return &mockAgent{}
}
// NewMockSession creates a new mock session
func NewMockSession() *mockSession {
return &mockSession{
sessionID: "mock-session-1",
memory: &mockMemory{},
}
}
func (s *mockSession) ID() string {
return s.sessionID
}
func (s *mockSession) Memory() Memory {
return s.memory
}
// Execute implements the Agent interface with mock responses
func (m *mockAgent) Execute(ctx context.Context, session Session, input Input, opts ...AgentExecutionContextOpt) (Output, error) {
// Simple mock responses based on input
query := strings.ToLower(input.Query)
var response string
switch {
case strings.Contains(query, "vulnerability") || strings.Contains(query, "vuln"):
response = `🔍 **Vulnerability Analysis**
I found 3 critical vulnerabilities in your dependencies:
**Critical Issues:**
lodash@4.17.19: CVE-2021-23337 (Command Injection)
jackson-databind@2.9.8: CVE-2020-36518 (Deserialization)
urllib3@1.24.1: CVE-2021-33503 (SSRF)
**Recommendation:** Update these packages immediately. All have fixes available in newer versions.
Would you like me to analyze the impact of updating these packages?`
case strings.Contains(query, "malware") || strings.Contains(query, "malicious"):
response = `🚨 **Malware Detection Results**
I detected 2 potentially malicious packages:
**High Risk:**
suspicious-package@1.0.0: Contains obfuscated code and cryptocurrency mining
typosquatted-lib@2.1.0: Mimics popular library with malicious payload
**Action Required:** Remove these packages immediately and scan your systems.
Would you like me to suggest secure alternatives?`
case strings.Contains(query, "secure") || strings.Contains(query, "security"):
response = `🛡 **Security Posture Assessment**
**Overall Security Score: 6.2/10 (Moderate Risk)**
**Summary:**
23 total security issues found
3 critical vulnerabilities requiring immediate action
2 malicious packages detected
15 packages with maintenance concerns
**Priority Actions:**
1. Remove malicious packages (Critical)
2. Update vulnerable dependencies (High)
3. Implement dependency scanning in CI/CD (Medium)
Would you like me to create a detailed remediation plan?`
case strings.Contains(query, "update"):
response = ` **Update Analysis**
Analyzing update recommendations for your dependencies...
**Safe Updates Available:**
12 packages can be safely updated (patch versions)
5 packages have minor version updates with new features
3 packages require major version updates (breaking changes)
**Priority Updates:**
1. lodash: 4.17.19 4.17.21 (Security fix, no breaking changes)
2. urllib3: 1.24.1 1.26.18 (Security fix, minimal risk)
Would you like detailed impact analysis for any specific package?`
default:
response = fmt.Sprintf(`🤖 **Security Analysis**
I'm analyzing your question about: "%s"
I have access to comprehensive security data including:
Vulnerability databases
Malware detection results
Dependency analysis
License compliance
Maintainer health metrics
**Available Analysis Types:**
Security posture assessment
Vulnerability impact analysis
Malware detection
Update recommendations
Compliance checking
What specific aspect would you like me to analyze in detail?`, input.Query)
}
return Output{
Answer: response,
Format: AnswerFormatMarkdown,
}, nil
}

164
agent/react.go Normal file
View File

@ -0,0 +1,164 @@
package agent
import (
"context"
"encoding/json"
"fmt"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/components/tool"
einoutils "github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
)
type ReactQueryAgentConfig struct {
MaxSteps int
SystemPrompt string
}
type reactQueryAgent struct {
config ReactQueryAgentConfig
model model.ToolCallingChatModel
tools []tool.BaseTool
}
var _ Agent = (*reactQueryAgent)(nil)
type reactQueryAgentOpt func(*reactQueryAgent)
func WithTools(tools []tool.BaseTool) reactQueryAgentOpt {
return func(a *reactQueryAgent) {
a.tools = tools
}
}
func NewReactQueryAgent(model model.ToolCallingChatModel,
config ReactQueryAgentConfig, opts ...reactQueryAgentOpt,
) (*reactQueryAgent, error) {
a := &reactQueryAgent{
config: config,
model: model,
}
for _, opt := range opts {
opt(a)
}
if a.config.MaxSteps == 0 {
a.config.MaxSteps = 30
}
return a, nil
}
func (a *reactQueryAgent) Execute(ctx context.Context, session Session, input Input, opts ...AgentExecutionContextOpt) (Output, error) {
executionContext := &AgentExecutionContext{}
for _, opt := range opts {
opt(executionContext)
}
agent, err := react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: a.model,
ToolsConfig: compose.ToolsNodeConfig{
Tools: a.wrapToolsForError(a.tools),
ToolArgumentsHandler: func(ctx context.Context, name string, arguments string) (string, error) {
// Only allow introspection if the function is provided. Do not allow mutation.
if executionContext.OnToolCall != nil {
_ = executionContext.OnToolCall(ctx, session, input, name, arguments)
}
return arguments, nil
},
},
MaxStep: a.config.MaxSteps,
})
if err != nil {
return Output{}, fmt.Errorf("failed to create react agent: %w", err)
}
var messages []*schema.Message
// Start with the system prompt if available
if a.config.SystemPrompt != "" {
messages = append(messages, &schema.Message{
Role: schema.System,
Content: a.config.SystemPrompt,
})
}
// Add the previous interactions to the messages
interactions, err := session.Memory().GetInteractions(ctx)
if err != nil {
return Output{}, fmt.Errorf("failed to get session memory: %w", err)
}
// TODO: Add a limit to the number of interactions to avoid context bloat
messages = append(messages, interactions...)
// Add the current user query message to the messages
userQueryMsg := &schema.Message{
Role: schema.User,
Content: input.Query,
}
messages = append(messages, userQueryMsg)
// Execute the agent to produce a response
msg, err := agent.Generate(ctx, messages)
if err != nil {
return Output{}, fmt.Errorf("failed to generate response: %w", err)
}
// Add the user query message to the session memory
err = session.Memory().AddInteraction(ctx, userQueryMsg)
if err != nil {
return Output{}, fmt.Errorf("failed to add user query message to session memory: %w", err)
}
// Add the agent response message to the session memory
err = session.Memory().AddInteraction(ctx, msg)
if err != nil {
return Output{}, fmt.Errorf("failed to add response message to session memory: %w", err)
}
return Output{
Answer: a.schemaContent(msg),
}, nil
}
func (a *reactQueryAgent) wrapToolsForError(tools []tool.BaseTool) []tool.BaseTool {
wrappedTools := make([]tool.BaseTool, len(tools))
for i, tool := range tools {
wrappedTools[i] = einoutils.WrapToolWithErrorHandler(tool, func(_ context.Context, err error) string {
errorMessage := map[string]string{
"error": err.Error(),
"suggestion": "Tool call failed, Please try a different approach or check your input.",
}
encodedError, err := json.Marshal(errorMessage)
if err != nil {
return ""
}
return string(encodedError)
})
}
return wrappedTools
}
func (a *reactQueryAgent) schemaContent(msg *schema.Message) string {
content := msg.Content
if len(msg.MultiContent) > 0 {
content = ""
for _, part := range msg.MultiContent {
content += part.Text + "\n"
}
}
return content
}

29
agent/session.go Normal file
View File

@ -0,0 +1,29 @@
package agent
import "github.com/google/uuid"
type session struct {
sessionID string
memory Memory
}
var _ Session = (*session)(nil)
func NewSession(memory Memory) (*session, error) {
return newSessionWithID(uuid.New().String(), memory), nil
}
func newSessionWithID(id string, memory Memory) *session {
return &session{
sessionID: id,
memory: memory,
}
}
func (s *session) ID() string {
return s.sessionID
}
func (s *session) Memory() Memory {
return s.memory
}

651
agent/ui.go Normal file
View File

@ -0,0 +1,651 @@
package agent
import (
"context"
"fmt"
"strings"
"time"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
)
// Message types for Bubbletea updates
type statusUpdateMsg struct {
message string
}
type agentResponseMsg struct {
content string
}
type agentThinkingMsg struct {
thinking bool
}
type agentToolCallMsg struct {
toolName string
toolArgs string
}
type thinkingTickMsg struct{}
var (
headerStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("240")).
Padding(0, 1)
inputPromptStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("240"))
inputCursorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("255"))
inputBorderStyle = lipgloss.NewStyle().
Border(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
Padding(0, 1)
thinkingStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("33")).
Bold(true)
)
// AgentUI represents the main TUI model
type agentUI struct {
viewport viewport.Model
textInput textarea.Model
width int
height int
statusMessage string
isThinking bool
messages []uiMessage
ready bool
agent Agent
session Session
config AgentUIConfig
thinkingFrame int
inputHistory []string
historyIndex int
currentInput string
}
// Message represents a chat message
type uiMessage struct {
Role string // "user", "agent", "system"
Content string
Timestamp time.Time
}
// AgentUIConfig defines the configuration for the UI
type AgentUIConfig struct {
Width int
Height int
InitialSystemMessage string
TextInputPlaceholder string
TitleText string
MaxHistory int
// Only for informational purposes.
ModelName string
ModelVendor string
ModelFast bool
}
// DefaultAgentUIConfig returns the opinionated default configuration for the UI
func DefaultAgentUIConfig() AgentUIConfig {
return AgentUIConfig{
Width: 80,
Height: 20,
MaxHistory: 50,
InitialSystemMessage: "Security Agent initialized",
TextInputPlaceholder: "Ask me anything...",
TitleText: "Security Agent",
}
}
// NewAgentUI creates a new agent UI instance
func NewAgentUI(agent Agent, session Session, config AgentUIConfig) *agentUI {
vp := viewport.New(config.Width, config.Height)
ta := textarea.New()
ta.Placeholder = ""
ta.Focus()
ta.SetHeight(1)
ta.SetWidth(80)
ta.CharLimit = 1000
ta.ShowLineNumbers = false
ui := &agentUI{
viewport: vp,
textInput: ta,
statusMessage: "",
messages: []uiMessage{},
agent: agent,
session: session,
config: config,
thinkingFrame: 0,
inputHistory: []string{},
historyIndex: -1,
currentInput: "",
}
ui.addSystemMessage(config.InitialSystemMessage)
return ui
}
// Init implements the tea.Model interface
func (m *agentUI) Init() tea.Cmd {
return tea.Batch(
textarea.Blink,
m.tickThinking(),
)
}
// Update implements the tea.Model interface
func (m *agentUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
return m, tea.Quit
case tea.KeyEnter:
if m.textInput.Focused() && !m.isThinking {
// Handle user input only if agent is not in thinking mode
userInput := strings.TrimSpace(m.textInput.Value())
if userInput != "" {
// Add to history and reset navigation
m.addToHistory(userInput)
// Add the input to the message list and reset user input field
m.addUserMessage(userInput)
m.resetInputField()
// Check if it's a slash command
if strings.HasPrefix(userInput, "/") {
// Handle slash command
cmd := m.handleSlashCommand(userInput)
if cmd != nil {
cmds = append(cmds, cmd)
}
} else {
// Execute agent query
cmds = append(cmds,
m.setThinking(true),
m.executeAgentQuery(userInput),
)
}
}
}
case tea.KeyTab:
// Switch focus between input and viewport, but not while agent is thinking
if !m.isThinking {
if m.textInput.Focused() {
m.textInput.Blur()
} else {
m.textInput.Focus()
cmds = append(cmds, textarea.Blink)
}
}
case tea.KeyUp, tea.KeyDown:
if m.textInput.Focused() && !m.isThinking {
// Navigate input history when text input is focused
var direction int
if msg.Type == tea.KeyUp {
direction = 1 // Go back in history
} else {
direction = -1 // Go forward in history
}
historyEntry := m.navigateHistory(direction)
m.textInput.SetValue(historyEntry)
m.textInput.CursorEnd()
} else if !m.textInput.Focused() {
// Allow scrolling in viewport when not focused on text input
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
}
case tea.KeyPgUp, tea.KeyPgDown:
// Allow scrolling in viewport when not focused on text input
if !m.textInput.Focused() {
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
}
case tea.KeyHome:
if !m.textInput.Focused() {
m.viewport.GotoTop()
}
case tea.KeyEnd:
if !m.textInput.Focused() {
m.viewport.GotoBottom()
}
}
case tea.WindowSizeMsg:
// Handle window resize
m.width = msg.Width
m.height = msg.Height
// Calculate dimensions for minimal UI
headerHeight := 2 // Header + blank line
inputHeight := 2 // Input area + status
spacing := 1 // Bottom spacing
// Calculate viewport dimensions to maximize output area
viewportHeight := m.height - headerHeight - inputHeight - spacing
// Ensure minimum height
if viewportHeight < 10 {
viewportHeight = 10
}
// Full width utilization
viewportWidth := m.width
// Ensure minimum width
if viewportWidth < 50 {
viewportWidth = 50
}
m.viewport.Width = viewportWidth
m.viewport.Height = viewportHeight
m.textInput.SetWidth(m.width - 3)
// Update content when dimensions change
m.viewport.SetContent(m.renderMessages())
if !m.ready {
m.ready = true
}
case statusUpdateMsg:
m.statusMessage = msg.message
case agentThinkingMsg:
m.isThinking = msg.thinking
// When agent starts thinking, blur the input
if m.isThinking {
m.resetInputField()
m.textInput.Blur()
m.thinkingFrame = 0
cmds = append(cmds, m.tickThinking())
} else {
// Re-focus input when thinking stops
m.textInput.Focus()
cmds = append(cmds, textarea.Blink)
}
case agentResponseMsg:
m.addAgentMessage(msg.content)
cmds = append(cmds, m.setThinking(false))
case agentToolCallMsg:
m.addToolCallMessage(fmt.Sprintf("🔧 %s", msg.toolName), msg.toolArgs)
case thinkingTickMsg:
if m.isThinking {
m.thinkingFrame = (m.thinkingFrame + 1) % 4
cmds = append(cmds, m.tickThinking())
}
}
// Update child components
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
// Only update text input if not thinking
if !m.isThinking {
m.textInput, cmd = m.textInput.Update(msg)
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}
// View implements the tea.Model interface
func (m *agentUI) View() string {
if !m.ready {
return "Loading..."
}
if m.width == 0 || m.height == 0 {
return "Initializing..."
}
modelAbility := "fast"
if !m.config.ModelFast {
modelAbility = "slow"
}
modelStatusLine := fmt.Sprintf("%s/%s (%s)", m.config.ModelVendor, m.config.ModelName, modelAbility)
header := headerStyle.Render(fmt.Sprintf("%s %s", m.config.TitleText, modelStatusLine))
content := m.viewport.View()
var thinkingIndicator string
if m.isThinking {
thinkingFrames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
spinner := thinkingFrames[m.thinkingFrame%len(thinkingFrames)]
thinkingIndicator = thinkingStyle.Render(fmt.Sprintf("%s thinking...", spinner))
}
var inputArea string
userInput := m.textInput.Value()
cursor := ""
if m.textInput.Focused() && !m.isThinking {
cursor = inputCursorStyle.Render("▊")
}
inputContent := fmt.Sprintf("%s%s%s", inputPromptStyle.Render("> "), userInput, cursor)
inputArea = inputBorderStyle.Width(m.width - 2).Render(inputContent)
statusLine := inputPromptStyle.Render(fmt.Sprintf("** %s | ctrl+c to exit", modelStatusLine))
var components []string
components = append(components, header, "", content, "")
if thinkingIndicator != "" {
components = append(components, thinkingIndicator)
}
components = append(components, inputArea, statusLine)
return lipgloss.JoinVertical(lipgloss.Left, components...)
}
func (m *agentUI) resetInputField() {
m.textInput.Reset()
m.textInput.SetValue("")
m.textInput.CursorStart()
}
func (m *agentUI) addUserMessage(content string) {
m.messages = append(m.messages, uiMessage{
Role: "user",
Content: content,
Timestamp: time.Now(),
})
m.viewport.SetContent(m.renderMessages())
m.viewport.GotoBottom()
}
func (m *agentUI) addAgentMessage(content string) {
m.messages = append(m.messages, uiMessage{
Role: "agent",
Content: content,
Timestamp: time.Now(),
})
m.viewport.SetContent(m.renderMessages())
m.viewport.GotoBottom()
}
func (m *agentUI) addSystemMessage(content string) {
m.messages = append(m.messages, uiMessage{
Role: "system",
Content: content,
Timestamp: time.Now(),
})
m.viewport.SetContent(m.renderMessages())
m.viewport.GotoBottom()
}
func (m *agentUI) addToolCallMessage(toolName string, toolArgs string) {
content := fmt.Sprintf(" %s", toolName)
if toolArgs != "" && toolArgs != "{}" {
content += fmt.Sprintf("\n └─ %s", toolArgs)
}
m.messages = append(m.messages, uiMessage{
Role: "tool",
Content: content,
Timestamp: time.Now(),
})
m.viewport.SetContent(m.renderMessages())
m.viewport.GotoBottom()
}
// renderMessages formats all messages for display
func (m *agentUI) renderMessages() string {
var rendered []string
rendered = append(rendered, "", "")
contentWidth := m.viewport.Width - 2 // Account for internal padding
if contentWidth < 40 {
contentWidth = 40
}
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle("notty"),
glamour.WithWordWrap(contentWidth),
)
if err != nil {
r = nil
}
for _, msg := range m.messages {
timestamp := msg.Timestamp.Format("15:04:05")
switch msg.Role {
case "user":
userHeaderStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("86")).
Bold(true).
Border(lipgloss.NormalBorder(), false, false, false, true).
BorderForeground(lipgloss.Color("86")).
Padding(0, 1)
userContentStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("255")).
Padding(0, 2)
rendered = append(rendered,
userHeaderStyle.Render(fmt.Sprintf("[%s] → You:", timestamp)),
userContentStyle.Render(msg.Content),
"",
)
case "agent":
var content string
if r != nil {
renderedMarkdown, err := r.Render(msg.Content)
if err == nil {
content = strings.TrimSpace(renderedMarkdown)
} else {
content = msg.Content // Fallback to plain text
}
} else {
content = msg.Content
}
agentHeaderStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("39")).
Bold(true).
Border(lipgloss.NormalBorder(), false, false, false, true).
BorderForeground(lipgloss.Color("39")).
Padding(0, 1)
agentContentStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("255")).
Padding(0, 2)
rendered = append(rendered,
agentHeaderStyle.Render(fmt.Sprintf("[%s] ← Agent:", timestamp)),
agentContentStyle.Render(content),
"",
)
case "system":
systemStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("241")).
Italic(true).
Border(lipgloss.NormalBorder(), false, false, false, true).
BorderForeground(lipgloss.Color("241")).
Padding(0, 1)
rendered = append(rendered,
systemStyle.Render(fmt.Sprintf("[%s] %s", timestamp, msg.Content)),
"",
)
case "tool":
toolStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("245")).
Italic(true).
Faint(true).
Border(lipgloss.NormalBorder(), false, false, false, true).
BorderForeground(lipgloss.Color("245")).
Padding(0, 1)
rendered = append(rendered,
toolStyle.Render(fmt.Sprintf("[%s] %s", timestamp, msg.Content)),
"",
)
}
}
rendered = append(rendered, "", "")
return strings.Join(rendered, "\n")
}
func (m *agentUI) updateStatus(message string) tea.Cmd {
return func() tea.Msg {
return statusUpdateMsg{message: message}
}
}
func (m *agentUI) setThinking(thinking bool) tea.Cmd {
return func() tea.Msg {
return agentThinkingMsg{thinking: thinking}
}
}
func (m *agentUI) executeAgentQuery(userInput string) tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
input := Input{
Query: userInput,
}
toolCallHook := func(_ context.Context, _ Session, _ Input, toolName string, toolArgs string) error {
m.Update(agentToolCallMsg{toolName: toolName, toolArgs: toolArgs})
return nil
}
output, err := m.agent.Execute(ctx, m.session, input, WithToolCallHook(toolCallHook))
if err != nil {
return agentResponseMsg{
content: fmt.Sprintf("❌ **Error**\n\nSorry, I encountered an error while processing your query:\n\n%s", err.Error()),
}
}
return agentResponseMsg{content: output.Answer}
}
}
// StartUI starts the TUI application with the default configuration
func StartUI(agent Agent, session Session) error {
config := DefaultAgentUIConfig()
config.InitialSystemMessage = ""
return StartUIWithConfig(agent, session, config)
}
// StartUIWithConfig starts the TUI application with the provided configuration
func StartUIWithConfig(agent Agent, session Session, config AgentUIConfig) error {
ui := NewAgentUI(agent, session, config)
p := tea.NewProgram(
ui,
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
_, err := p.Run()
return err
}
func (m *agentUI) tickThinking() tea.Cmd {
return tea.Tick(150*time.Millisecond, func(time.Time) tea.Msg {
return thinkingTickMsg{}
})
}
// handleSlashCommand processes commands that start with '/'
func (m *agentUI) handleSlashCommand(command string) tea.Cmd {
switch command {
case "/exit":
m.addSystemMessage("Goodbye! Exiting gracefully...")
return tea.Quit
default:
m.addSystemMessage(fmt.Sprintf("Unknown command: %s", command))
return nil
}
}
// addToHistory adds input to history buffer with a maximum of 50 entries
func (m *agentUI) addToHistory(input string) {
// Don't add empty strings or duplicates of the last entry
if input == "" || (len(m.inputHistory) > 0 && m.inputHistory[len(m.inputHistory)-1] == input) {
return
}
m.inputHistory = append(m.inputHistory, input)
// Keep only the last maxHistory entries
if len(m.inputHistory) > m.config.MaxHistory {
m.inputHistory = m.inputHistory[len(m.inputHistory)-m.config.MaxHistory:]
}
// Reset history navigation
m.historyIndex = -1
m.currentInput = ""
}
// navigateHistory moves through input history and returns the selected entry
func (m *agentUI) navigateHistory(direction int) string {
if len(m.inputHistory) == 0 {
return ""
}
// Save current input when starting navigation
if m.historyIndex == -1 {
m.currentInput = m.textInput.Value()
}
// Calculate new index
newIndex := m.historyIndex + direction
// Handle boundaries
if newIndex < -1 {
newIndex = -1
} else if newIndex >= len(m.inputHistory) {
newIndex = len(m.inputHistory) - 1
}
m.historyIndex = newIndex
// Return the appropriate entry
if m.historyIndex == -1 {
return m.currentInput
}
return m.inputHistory[len(m.inputHistory)-1-m.historyIndex]
}

280
agent/ui_test.go Normal file
View File

@ -0,0 +1,280 @@
package agent
import (
"testing"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/stretchr/testify/assert"
)
func TestAgentUICreation(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
assert.NotNil(t, ui, "Failed to create AgentUI")
assert.Empty(t, ui.statusMessage, "Expected empty status message")
assert.False(t, ui.isThinking, "UI should not be thinking initially")
assert.Equal(t, 0, ui.thinkingFrame, "Thinking frame should be 0 initially")
// Check that system message was added if config has one
if config.InitialSystemMessage != "" {
assert.NotEmpty(t, ui.messages, "Expected system message to be added if InitialSystemMessage is set")
}
}
func TestDefaultAgentUIConfig(t *testing.T) {
config := DefaultAgentUIConfig()
assert.Equal(t, 80, config.Width, "Expected default width 80")
assert.Equal(t, 20, config.Height, "Expected default height 20")
assert.Equal(t, "Security Agent", config.TitleText, "Expected title 'Security Agent'")
assert.Equal(t, "Ask me anything...", config.TextInputPlaceholder, "Expected placeholder 'Ask me anything...'")
}
func TestMessageManagement(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
initialCount := len(ui.messages)
// Test adding user message
ui.addUserMessage("Test user message")
assert.Equal(t, initialCount+1, len(ui.messages), "Expected message count to increase")
lastMessage := ui.messages[len(ui.messages)-1]
assert.Equal(t, "user", lastMessage.Role, "Expected last message role to be 'user'")
assert.Equal(t, "Test user message", lastMessage.Content, "Expected last message content to be 'Test user message'")
// Test adding agent message
ui.addAgentMessage("Test agent response")
assert.Equal(t, initialCount+2, len(ui.messages), "Expected message count to increase")
lastMessage = ui.messages[len(ui.messages)-1]
assert.Equal(t, "agent", lastMessage.Role, "Expected last message role to be 'agent'")
// Test adding system message
ui.addSystemMessage("System notification")
assert.Equal(t, initialCount+3, len(ui.messages), "Expected message count to increase")
lastMessage = ui.messages[len(ui.messages)-1]
assert.Equal(t, "system", lastMessage.Role, "Expected last message role to be 'system'")
// Test adding tool call message
ui.addToolCallMessage("ScanVulnerabilities", `{"path": "/app"}`)
assert.Equal(t, initialCount+4, len(ui.messages), "Expected message count to increase")
lastMessage = ui.messages[len(ui.messages)-1]
assert.Equal(t, "tool", lastMessage.Role, "Expected last message role to be 'tool'")
}
func TestMessageRendering(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
// Set up viewport dimensions for rendering
ui.viewport.Width = 80
ui.viewport.Height = 20
ui.addUserMessage("How many vulnerabilities?")
ui.addAgentMessage("Found 5 critical vulnerabilities")
rendered := ui.renderMessages()
assert.NotEmpty(t, rendered, "Expected non-empty rendered output")
assert.Contains(t, rendered, "How many vulnerabilities?", "Rendered output should contain user message")
assert.Contains(t, rendered, "Found 5 critical vulnerabilities", "Rendered output should contain agent message")
assert.Contains(t, rendered, "You:", "Rendered output should contain user label")
assert.Contains(t, rendered, "Agent:", "Rendered output should contain agent label")
}
func TestViewportDimensions(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
// Test window resize handling
resizeMsg := tea.WindowSizeMsg{Width: 100, Height: 30}
ui.Update(resizeMsg)
assert.Equal(t, 100, ui.width, "Expected width 100")
assert.Equal(t, 30, ui.height, "Expected height 30")
// Test minimum dimensions enforcement
resizeMsg = tea.WindowSizeMsg{Width: 10, Height: 5}
ui.Update(resizeMsg)
assert.GreaterOrEqual(t, ui.viewport.Width, 50, "Viewport width should be enforced to minimum 50")
assert.GreaterOrEqual(t, ui.viewport.Height, 10, "Viewport height should be enforced to minimum 10")
}
func TestViewRendering(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
config.ModelName = "gpt-4"
config.ModelVendor = "openai"
config.ModelFast = false
ui := NewAgentUI(mockAgent, mockSession, config)
ui.width = 80
ui.height = 24
ui.ready = true
view := ui.View()
assert.Contains(t, view, "Security Agent", "View should contain title")
assert.Contains(t, view, "openai/gpt-4", "View should contain model information")
assert.Contains(t, view, ">", "View should contain input prompt")
assert.Contains(t, view, "ctrl+c to exit", "View should contain exit instruction")
}
func TestThinkingState(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
ui.width = 80
ui.height = 24
ui.ready = true
// Initially not thinking
assert.False(t, ui.isThinking, "UI should not be thinking initially")
// Set thinking state
thinkingMsg := agentThinkingMsg{thinking: true}
ui.Update(thinkingMsg)
assert.True(t, ui.isThinking, "UI should be thinking after agentThinkingMsg")
// Check view contains thinking indicator
view := ui.View()
assert.Contains(t, view, "thinking...", "View should contain thinking indicator when thinking")
// Stop thinking
thinkingMsg = agentThinkingMsg{thinking: false}
ui.Update(thinkingMsg)
assert.False(t, ui.isThinking, "UI should not be thinking after agentThinkingMsg with false")
}
func TestKeyboardHandling(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
ui.width = 80
ui.height = 24
ui.ready = true
var keyMsg tea.KeyMsg
var model tea.Model
var cmd tea.Cmd
// Test Ctrl+C exits immediately
keyMsg = tea.KeyMsg{Type: tea.KeyCtrlC}
_, cmd = ui.Update(keyMsg)
assert.NotNil(t, cmd, "Ctrl+C should return quit command")
// Test Tab key for focus switching when not thinking
ui.textInput.Focus()
keyMsg = tea.KeyMsg{Type: tea.KeyTab}
model, _ = ui.Update(keyMsg)
ui = model.(*agentUI)
assert.False(t, ui.textInput.Focused(), "Tab should blur text input when it's focused")
// Test Enter key handling when not thinking
ui.textInput.Focus()
ui.textInput.SetValue("test message")
initialMessageCount := len(ui.messages)
keyMsg = tea.KeyMsg{Type: tea.KeyEnter}
model, _ = ui.Update(keyMsg)
ui = model.(*agentUI)
assert.Equal(t, initialMessageCount+1, len(ui.messages), "Enter should add user message when input is not empty")
// Note: Input field reset happens when thinking starts, not immediately
// The resetInputField() is called, but the UI state may not reflect it immediately in tests
}
func TestInputFieldReset(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
// Set some input text
ui.textInput.SetValue("test input")
assert.Equal(t, "test input", ui.textInput.Value(), "Input should contain test text")
// Reset input field
ui.resetInputField()
assert.Empty(t, ui.textInput.Value(), "Input should be empty after reset")
}
func TestCommandCreation(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
// Test status update command
cmd := ui.updateStatus("Testing status")
assert.NotNil(t, cmd, "updateStatus should return a non-nil command")
// Test thinking command
cmd = ui.setThinking(true)
assert.NotNil(t, cmd, "setThinking should return a non-nil command")
// Test execute agent query command
cmd = ui.executeAgentQuery("test query")
assert.NotNil(t, cmd, "executeAgentQuery should return a non-nil command")
}
func TestMessageTimestamps(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
before := time.Now()
ui.addUserMessage("Test message")
after := time.Now()
message := ui.messages[len(ui.messages)-1]
assert.True(t, message.Timestamp.After(before) || message.Timestamp.Equal(before), "Message timestamp should be after or equal to before time")
assert.True(t, message.Timestamp.Before(after) || message.Timestamp.Equal(after), "Message timestamp should be before or equal to after time")
}
func TestUIInitialization(t *testing.T) {
mockAgent := NewMockAgent()
mockSession := NewMockSession()
config := DefaultAgentUIConfig()
ui := NewAgentUI(mockAgent, mockSession, config)
// Test Init command
cmd := ui.Init()
assert.NotNil(t, cmd, "Init should return a non-nil command")
// Test initial state before ready
view := ui.View()
assert.Equal(t, "Loading...", view, "View should show loading before ready")
// Test with zero dimensions
ui.ready = true
ui.width = 0
ui.height = 0
view = ui.View()
assert.Equal(t, "Initializing...", view, "View should show initializing with zero dimensions")
}

53
cmd/agent/common.go Normal file
View File

@ -0,0 +1,53 @@
package agent
import (
"context"
"fmt"
"os"
"github.com/charmbracelet/glamour"
"github.com/safedep/vet/agent"
)
func buildModelFromEnvironment() (*agent.Model, error) {
model, err := agent.BuildModelFromEnvironment(fastMode)
if err != nil {
return nil, fmt.Errorf("failed to build LLM model adapter using environment configuration: %w", err)
}
return model, nil
}
func executeAgentPrompt(agentExecutor agent.Agent, session agent.Session, prompt string) error {
output, err := agentExecutor.Execute(context.Background(), session, agent.Input{
Query: prompt,
}, agent.WithToolCallHook(func(ctx context.Context, session agent.Session, input agent.Input, toolName string, toolArgs string) error {
os.Stderr.WriteString(fmt.Sprintf("Tool called: %s with args: %s\n", toolName, toolArgs))
return nil
}))
if err != nil {
return fmt.Errorf("failed to execute agent: %w", err)
}
terminalRenderer, err := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(80),
glamour.WithEmoji(),
)
if err != nil {
return fmt.Errorf("failed to create glamour renderer: %w", err)
}
rendered, err := terminalRenderer.Render(output.Answer)
if err != nil {
return fmt.Errorf("failed to render answer: %w", err)
}
_, err = os.Stdout.WriteString(rendered)
if err != nil {
return fmt.Errorf("failed to write answer: %w", err)
}
return nil
}

34
cmd/agent/main.go Normal file
View File

@ -0,0 +1,34 @@
// Package agent provides a CLI for running agents.
package agent
import "github.com/spf13/cobra"
var (
maxAgentSteps int
// Use a fast model when available. Opinionated. Can be overridden by the
// setting environment variables.
fastMode bool
// User wants the agent to answer a single question and not start the
// interactive agent. Not all agents may support this.
singlePrompt string
)
func NewAgentCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "agent",
Short: "Run an available AI agent",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.PersistentFlags().IntVar(&maxAgentSteps, "max-steps", 30, "The maximum number of steps for the agent executor")
cmd.PersistentFlags().StringVarP(&singlePrompt, "prompt", "p", "", "A single prompt to run the agent with")
cmd.PersistentFlags().BoolVar(&fastMode, "fast", false, "Prefer a fast model when available (compromises on advanced reasoning)")
cmd.AddCommand(newQueryAgentCommand())
return cmd
}

107
cmd/agent/query.go Normal file
View File

@ -0,0 +1,107 @@
package agent
import (
"context"
_ "embed"
"fmt"
"github.com/spf13/cobra"
"github.com/safedep/vet/agent"
"github.com/safedep/vet/internal/analytics"
"github.com/safedep/vet/internal/command"
"github.com/safedep/vet/pkg/common/logger"
)
//go:embed query_prompt.md
var querySystemPrompt string
var queryAgentDBPath string
func newQueryAgentCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "query",
Short: "Query agent allows analysis and querying the vet sqlite3 report database",
RunE: func(cmd *cobra.Command, args []string) error {
err := executeQueryAgent()
if err != nil {
logger.Errorf("failed to execute query agent: %v", err)
}
return nil
},
}
cmd.Flags().StringVar(&queryAgentDBPath, "db", "", "The path to the vet sqlite3 report database")
_ = cmd.MarkFlagRequired("db")
return cmd
}
func executeQueryAgent() error {
analytics.TrackAgentQuery()
toolBuilder, err := agent.NewMcpClientToolBuilder(agent.McpClientToolBuilderConfig{
ClientName: "vet-query-agent",
ClientVersion: command.GetVersion(),
SkipDefaultTools: true,
SQLQueryToolEnabled: true,
SQLQueryToolDBPath: queryAgentDBPath,
PackageRegistryToolEnabled: true,
})
if err != nil {
return fmt.Errorf("failed to create MCP client tool builder: %w", err)
}
tools, err := toolBuilder.Build(context.Background())
if err != nil {
return fmt.Errorf("failed to build tools: %w", err)
}
model, err := buildModelFromEnvironment()
if err != nil {
return fmt.Errorf("failed to build LLM model adapter using environment configuration: %w", err)
}
agentExecutor, err := agent.NewReactQueryAgent(model.Client, agent.ReactQueryAgentConfig{
MaxSteps: maxAgentSteps,
SystemPrompt: querySystemPrompt,
}, agent.WithTools(tools))
if err != nil {
return fmt.Errorf("failed to create agent: %w", err)
}
memory, err := agent.NewSimpleMemory()
if err != nil {
return fmt.Errorf("failed to create memory: %w", err)
}
session, err := agent.NewSession(memory)
if err != nil {
return fmt.Errorf("failed to create session: %w", err)
}
if singlePrompt != "" {
err = executeAgentPrompt(agentExecutor, session, singlePrompt)
if err != nil {
return fmt.Errorf("failed to execute agent prompt: %w", err)
}
} else {
uiConfig := agent.DefaultAgentUIConfig()
uiConfig.TitleText = "🔍 Query Agent - Interactive Query Mode"
uiConfig.TextInputPlaceholder = "Ask me anything about your scan data..."
uiConfig.InitialSystemMessage = "🤖 Query Agent initialized. Ask me anything about your dependencies, vulnerabilities and other supply chain risks."
uiConfig.ModelName = model.Name
uiConfig.ModelVendor = model.Vendor
uiConfig.ModelFast = model.Fast
err = agent.StartUIWithConfig(agentExecutor, session, uiConfig)
if err != nil {
return fmt.Errorf("failed to start agent interaction UI: %w", err)
}
}
return nil
}

57
cmd/agent/query_prompt.md Normal file
View File

@ -0,0 +1,57 @@
Your task is to assist the user in finding useful information from vet scan results
available in an sqlite3 database.
To answer user's query, you MUST do the following:
1. **Schema Discovery**: Use the database schema introspection tool to understand the available tables, columns, and relationships
2. **Query Planning**: Analyze the user's question and plan your approach:
- Identify which tables contain the relevant data
- Determine the relationships between tables needed
- Plan the query structure before writing SQL
3. **Query Execution**: Execute your planned query using the database query tool
4. **Result Validation**: Verify the results make sense and answer the user's question
5. **Response Formatting**: Present findings in clear markdown format
GUIDELINES:
* **Query Best Practices**:
- Always use `COUNT(*)` instead of `SELECT *` when determining table sizes
- Use `LIMIT` and `OFFSET` for pagination with large result sets
- Prefer JOINs over subqueries for better performance
- Use aggregate functions (COUNT, SUM, AVG) for statistical queries
* **Data Integrity**:
- NEVER make assumptions about data that you haven't verified through queries
- If a query returns unexpected results, re-examine your approach
- Always check for NULL values and handle them appropriately
- Validate that your query logic matches the user's intent
* **Error Handling**:
- If a query fails, explain the error and try an alternative approach
- If no data is found, clearly state this rather than making assumptions
- When data seems incomplete, acknowledge limitations in your response
IMPORTANT CONSTRAINTS:
* **Prevent Hallucinations**:
- Only report data that you have actually queried from the database
- NEVER invent or assume data points that weren't returned by your queries
- If you're unsure about a result, query the data again to confirm
- Always distinguish between actual data and your interpretation of it
* **User Interaction**:
- Ask for clarification if the user's query is ambiguous
- Provide context about what the data represents (e.g., "This shows vulnerabilities found in your dependencies")
- If you cannot answer with available data, explain what information is missing
* **Response Format**:
- Present tabular data as markdown tables with appropriate headers
- Include summary statistics when relevant (e.g., "Found 15 vulnerabilities across 8 packages")
- Use clear headings to organize complex responses
- Always explain what the data means in the context of security scanning
* **Domain Context**:
- Remember that vet scans analyze software dependencies for security issues
- Common entities include: packages, vulnerabilities, licenses, malware, scorecards
- Explain technical terms that may be unfamiliar to users

View File

@ -3,11 +3,12 @@ package cloud
import (
"time"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/cloud"
"github.com/safedep/vet/pkg/common/logger"
"github.com/spf13/cobra"
)
var (
@ -178,7 +179,6 @@ func executeCreateKey() error {
Desc: keyDescription,
ExpiryInDays: keyExpiresIn,
})
if err != nil {
return err
}

View File

@ -5,11 +5,13 @@ import (
"fmt"
"net/http"
"github.com/cli/oauth/api"
"github.com/cli/oauth/device"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/common/logger"
"github.com/spf13/cobra"
)
func newCloudLoginCommand() *cobra.Command {
@ -17,7 +19,7 @@ func newCloudLoginCommand() *cobra.Command {
Use: "login",
Short: "Login to SafeDep cloud for management tasks",
RunE: func(cmd *cobra.Command, args []string) error {
err := executeDeviceAuthFlow()
err := executeCloudLogin()
if err != nil {
logger.Errorf("Failed to login to the SafeDep cloud: %v", err)
}
@ -29,14 +31,24 @@ func newCloudLoginCommand() *cobra.Command {
return cmd
}
func executeDeviceAuthFlow() error {
func executeCloudLogin() error {
token, err := executeDeviceAuthFlow()
if err != nil {
return fmt.Errorf("failed to execute device auth flow: %w", err)
}
return auth.PersistCloudTokens(token.Token,
token.RefreshToken, tenantDomain)
}
func executeDeviceAuthFlow() (*api.AccessToken, error) {
code, err := device.RequestCode(http.DefaultClient,
auth.CloudIdentityServiceDeviceCodeUrl(),
auth.CloudIdentityServiceClientId(),
[]string{"offline_access", "openid", "profile", "email"},
device.WithAudience(auth.CloudIdentityServiceAudience()))
if err != nil {
return fmt.Errorf("failed to request device code: %w", err)
return nil, fmt.Errorf("failed to request device code: %w", err)
}
ui.PrintSuccess("Please visit %s and enter the code %s to authenticate",
@ -49,9 +61,8 @@ func executeDeviceAuthFlow() error {
DeviceCode: code,
})
if err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
return nil, fmt.Errorf("failed to authenticate: %w", err)
}
return auth.PersistCloudTokens(token.Token,
token.RefreshToken, tenantDomain)
return token, nil
}

View File

@ -1,8 +1,12 @@
package cloud
import (
"github.com/safedep/vet/internal/auth"
"fmt"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
)
var (
@ -31,10 +35,24 @@ func NewCloudCommand() *cobra.Command {
cmd.AddCommand(newCloudLoginCommand())
cmd.AddCommand(newRegisterCommand())
cmd.AddCommand(newQueryCommand())
cmd.AddCommand(newPingCommand())
cmd.AddCommand(newWhoamiCommand())
cmd.AddCommand(newKeyCommand())
cmd.AddCommand(newCloudQuickstartCommand())
queryCmd := newQueryCommand()
queryCmd.PreRunE = requireAccessTokenCheck
pingCmd := newPingCommand()
pingCmd.PreRunE = requireAccessTokenCheck
whoamiCmd := newWhoamiCommand()
whoamiCmd.PreRunE = requireAccessTokenCheck
keyCmd := newKeyCommand()
keyCmd.PreRunE = requireAccessTokenCheck
cmd.AddCommand(queryCmd)
cmd.AddCommand(pingCmd)
cmd.AddCommand(whoamiCmd)
cmd.AddCommand(keyCmd)
cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
if tenantDomain != "" {
@ -44,3 +62,26 @@ func NewCloudCommand() *cobra.Command {
return cmd
}
func requireAccessTokenCheck(cmd *cobra.Command, args []string) error {
// Check if token was obtained/refreshed 5 mins ago
// If > 5 mins, check the access token expiry
// else return
if auth.ShouldCheckAccessTokenExpiry() {
// Check if access token is expired
// If expired (ok), refresh the session
if ok, err := auth.IsAccessTokenExpired(); err != nil {
tenantDomainPlaceholder := auth.TenantDomain()
if tenantDomainPlaceholder == "" {
tenantDomainPlaceholder = "<your-tenant-domain>"
}
ui.PrintError("Automatic token refresh failed, please re-login using `vet cloud login --tenant %s`", tenantDomainPlaceholder)
return fmt.Errorf("failed to check access token expiry: %w", err)
} else if ok {
ui.PrintMsg("Refreshing Access Token")
return auth.RefreshCloudSession()
}
}
return nil
}

View File

@ -3,11 +3,12 @@ package cloud
import (
"time"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/cloud"
"github.com/safedep/vet/pkg/common/logger"
"github.com/spf13/cobra"
)
func newPingCommand() *cobra.Command {

View File

@ -4,11 +4,12 @@ import (
"errors"
"sort"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/cloud/query"
"github.com/safedep/vet/pkg/common/logger"
"github.com/spf13/cobra"
)
var (

314
cmd/cloud/quickstart.go Normal file
View File

@ -0,0 +1,314 @@
package cloud
import (
"fmt"
"os"
"time"
controltowerv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/controltower/v1"
"github.com/AlecAivazis/survey/v2"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/cloud"
)
func newCloudQuickstartCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "quickstart",
Short: "Quick onboarding to SafeDep Cloud and cli setup",
RunE: func(cmd *cobra.Command, args []string) error {
err := executeCloudQuickstart()
if err != nil {
os.Exit(1)
}
return nil
},
}
return cmd
}
// executeCloudQuickstart executes an opinionated quick start flow for the user
// with the goal of least friction on-boarding to SafeDep Cloud and configuring
// the cli with everything required to start using SafeDep Cloud services.
func executeCloudQuickstart() error {
ui.PrintMsg("🚀 Starting SafeDep Cloud Quickstart...")
ui.PrintMsg("👋 Hello! Let's get you onboarded..")
// This will execute cloud authentication flow and persist the cloud tokens
// in the local config file.
if err := quickStartAuthentication(); err != nil {
return err
}
// Here we create a connection to the control plane with cloud token. This
// connection may not be multi-tenant because user may not have any tenants
// yet.
conn, err := quickStartCreateConnection()
if err != nil {
return err
}
// Here we check if the user has any tenants. If not, we create a new one.
userInfo, err := quickStartTenantSetup(conn)
if err != nil {
return err
}
// Here we get the tenant from the user info. The tenant domain is stored
// in the local config file.
tenant, err := quickStartSetupTenantFromAccess(userInfo)
if err != nil {
return err
}
ui.PrintMsg("✅ Your tenant is set to: %s", tenant.GetDomain())
// Close the previous connection
if err := conn.Close(); err != nil {
ui.PrintError("❌ Oops! Something went wrong while closing cloud connection: %s", err.Error())
return err
}
// Here we re-create the connection because we need a multi-tenant connection
conn, err = quickStartCreateConnection()
if err != nil {
return err
}
if err := quickStartAPIKeyCreation(conn, tenant); err != nil {
return err
}
ui.PrintMsg("✅ All done!")
ui.PrintMsg("")
ui.PrintMsg("🎉 You are all set! You can now start using SafeDep Cloud")
// TODO: We need the ability to auto-detect the project name and version
// and then use that to sync the results to SafeDep Cloud
ui.PrintMsg("✨ Run `vet scan -D /path/to/code --report-sync` to scan your code and sync the results to SafeDep Cloud")
return nil
}
func quickStartSetupTenantFromAccess(userInfo *controltowerv1.GetUserInfoResponse) (*controltowerv1pb.Tenant, error) {
if len(userInfo.GetAccess()) == 0 {
ui.PrintError("❌ Oops! This is weird, you should have access to at least one tenant. Please contact support.")
return nil, fmt.Errorf("no tenant access")
}
// If user has access to multiple tenants
// Ask user about which tenant they want to use if they have more than one
var tenant *controltowerv1pb.Tenant
if len(userInfo.GetAccess()) > 1 {
// Print all tenants with index
var tenantOptions []string
ui.PrintMsg("🔍 You have access to the following tenants:")
for idx, tenant := range userInfo.GetAccess() {
ui.PrintMsg("%s", fmt.Sprintf(" - [%d] %s", idx, tenant.GetTenant().GetDomain()))
tenantOptions = append(tenantOptions, tenant.GetTenant().GetDomain())
}
// Ask user which tenant they want to use
var tenantIndex int
err := survey.AskOne(&survey.Select{
Message: "🔍 Which tenant do you want to use?",
Options: tenantOptions,
}, &tenantIndex)
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while asking which tenant to use: %s", err.Error())
return nil, err
}
tenant = userInfo.GetAccess()[tenantIndex].GetTenant()
} else {
tenant = userInfo.GetAccess()[0].GetTenant()
}
if err := auth.PersistTenantDomain(tenant.GetDomain()); err != nil {
ui.PrintError("❌ Oops! Something went wrong while persisting your tenant domain: %s", err.Error())
return nil, err
}
return tenant, nil
}
func quickStartAuthentication() error {
ui.PrintMsg("🔑 Start by creating an account or sign-in to your existing account")
token, err := executeDeviceAuthFlow()
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while authenticating you: %s", err.Error())
ui.PrintMsg(" If you are using email and password, ensure your email is verified.")
return err
}
ui.PrintSuccess("✅ Successfully authenticated you!")
ui.PrintMsg("🔑 Saving your cloud credentials in your local config...")
if err := auth.PersistCloudTokens(token.Token, token.RefreshToken, ""); err != nil {
ui.PrintError("❌ Oops! Something went wrong while saving your cloud credentials: %s", err.Error())
return err
}
ui.PrintSuccess("✅ Successfully saved your cloud credentials!")
return nil
}
func quickStartCreateConnection() (*grpc.ClientConn, error) {
conn, err := auth.ControlPlaneClientConnection("vet-cloud-quickstart")
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while creating cloud connection: %s", err.Error())
return nil, err
}
return conn, nil
}
func quickStartTenantSetup(conn *grpc.ClientConn) (*controltowerv1.GetUserInfoResponse, error) {
ui.PrintMsg("🔍 Checking if you have an existing tenant...")
userService, err := cloud.NewUserService(conn)
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while creating user service: %s", err.Error())
return nil, err
}
userInfo, err := userService.CurrentUserInfo()
if err != nil {
return quickStartCreateNewTenant(conn)
}
ui.PrintMsg("✅ You are already registered with SafeDep Cloud")
return userInfo, nil
}
func quickStartCreateNewTenant(conn *grpc.ClientConn) (*controltowerv1.GetUserInfoResponse, error) {
ui.PrintMsg("📝 Looks like you don't have an existing tenant. Let's create one for you...")
userName, domain, err := quickStartGetTenantInputs()
if err != nil {
return nil, err
}
onboardingService, err := cloud.NewOnboardingService(conn)
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while creating onboarding service: %s", err.Error())
return nil, err
}
_, err = onboardingService.Register(&cloud.RegisterRequest{
Name: userName,
Email: registerEmail,
OrgName: "Quickstart Organization",
OrgDomain: domain,
})
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while registering your tenant: %s", err.Error())
return nil, err
}
ui.PrintSuccess("✅ Successfully created a new tenant!")
ui.PrintMsg("🔑 Please wait while we get you onboarded...")
userService, err := cloud.NewUserService(conn)
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while creating user service: %s", err.Error())
return nil, err
}
return userService.CurrentUserInfo()
}
func quickStartGetTenantInputs() (string, string, error) {
var userName string
err := survey.AskOne(&survey.Input{
Message: "👤 What should we call you?",
Default: "John Doe",
}, &userName)
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while asking for your name: %s", err.Error())
return "", "", err
}
autoDomain := fmt.Sprintf("quickstart-%s", time.Now().Format("20060102150405"))
var domain string
err = survey.AskOne(&survey.Input{
Message: "📝 We have automatically generated a domain for you. Here is your chance to update",
Default: autoDomain,
}, &domain)
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while asking for your domain: %s", err.Error())
return "", "", err
}
if domain == "" {
domain = autoDomain
}
return userName, domain, nil
}
func quickStartAPIKeyCreation(conn *grpc.ClientConn, tenant *controltowerv1pb.Tenant) error {
var createAPIKey bool
err := survey.AskOne(&survey.Confirm{
Message: "🔑 Do you want to create a new API key for this tenant?",
Default: true,
}, &createAPIKey)
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while asking if you want to create an API key: %s", err.Error())
return err
}
if !createAPIKey {
return nil
}
var showAPIKey bool
err = survey.AskOne(&survey.Confirm{
Message: "Would you like to see the API key in addition to configuring it?",
Default: true,
}, &showAPIKey)
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while asking about showing the API key: %s", err.Error())
return err
}
createApiKeyService, err := cloud.NewApiKeyService(conn)
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while creating the API key service: %s", err.Error())
return err
}
apiKey, err := createApiKeyService.CreateApiKey(&cloud.CreateApiKeyRequest{
Name: fmt.Sprintf("Quick Start API Key: %s", time.Now().Format("20060102150405")),
Desc: "This is a quick start API key created for you by vet",
ExpiryInDays: 30,
})
if err != nil {
ui.PrintError("❌ Oops! Something went wrong while creating the API key: %s", err.Error())
return err
}
if err := auth.PersistApiKey(apiKey.Key, tenant.GetDomain()); err != nil {
ui.PrintError("❌ Oops! Something went wrong while persisting the API key: %s", err.Error())
return err
}
if showAPIKey {
ui.PrintMsg("✅ Here is your API key: %s", text.BgGreen.Sprint(apiKey.Key))
ui.PrintMsg("🔒 Your key will expire on: %s", apiKey.ExpiresAt.Format(time.RFC3339))
}
ui.PrintMsg(" Your tenant domain is: %s", text.BgGreen.Sprint(tenant.GetDomain()))
ui.PrintMsg("🔑 Please save this API key in a secure location, it will not be shown again.")
return nil
}

View File

@ -3,11 +3,12 @@ package cloud
import (
"fmt"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/cloud"
"github.com/safedep/vet/pkg/common/logger"
"github.com/spf13/cobra"
)
var (
@ -60,7 +61,6 @@ func registerUserTenant() error {
OrgName: registerOrgName,
OrgDomain: registerOrgDomain,
})
if err != nil {
return err
}

View File

@ -4,11 +4,12 @@ import (
"fmt"
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/cloud"
"github.com/safedep/vet/pkg/common/logger"
"github.com/spf13/cobra"
)
func newWhoamiCommand() *cobra.Command {

View File

@ -3,6 +3,7 @@ package code
import (
"github.com/safedep/code/core"
"github.com/safedep/code/lang"
"github.com/safedep/vet/pkg/common/logger"
)

View File

@ -1,8 +1,9 @@
package code
import (
"github.com/safedep/vet/internal/command"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/command"
)
var languageCodes []string
@ -10,7 +11,7 @@ var languageCodes []string
func NewCodeCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "code",
Short: "Analyze souce code",
Short: "Analyze source code",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},

View File

@ -4,12 +4,13 @@ import (
"context"
"regexp"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/command"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/code"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/storage"
"github.com/spf13/cobra"
)
var (
@ -81,7 +82,7 @@ func internalStartScan() error {
},
OnScanEnd: func() error {
ui.StopSpinner()
ui.PrintSuccess("Code scanning completed")
ui.PrintSuccess("🚀 Code scanning completed. Run vet scan with code context using --code flag")
return nil
},
},

72
cmd/doc/generate.go Normal file
View File

@ -0,0 +1,72 @@
package doc
import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
var (
// markdownOutDir is the output directory for markdown doc files
markdownOutDir string
// manOutDir is the output directory for troff (man markup) doc files
manOutDir string
)
func newGenerateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "generate",
Short: "Generate docs / manual artifacts",
RunE: func(cmd *cobra.Command, args []string) error {
// we specify the root (see, not parent) command since its the starting point for docs
return runGenerateCommand(cmd.Root())
},
}
cmd.PersistentFlags().StringVar(&markdownOutDir, "markdown", "", "The output directory for markdown doc files")
cmd.PersistentFlags().StringVar(&manOutDir, "man", "", "The output directory for troff (man markup) doc files")
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
// At least one of the output directory is required
if markdownOutDir == "" && manOutDir == "" {
return errors.New("no output directory specified, at least one of the output directory is required")
}
return nil
}
return cmd
}
func runGenerateCommand(rootCmd *cobra.Command) error {
// If markdown directory is specified
if markdownOutDir != "" {
// Create Markdown Manual
if err := doc.GenMarkdownTree(rootCmd, markdownOutDir); err != nil {
return errors.Wrap(err, "failed to generate markdown manual")
}
fmt.Println("Markdown manual doc created in: ", markdownOutDir)
}
// If troff (man markup) directory is specified
if manOutDir != "" {
// Create Troff (man markup) Manual
manHeader := &doc.GenManHeader{
Title: "VET",
Source: "SafeDep",
Manual: "VET Manual",
}
if err := doc.GenManTree(rootCmd, manHeader, manOutDir); err != nil {
return errors.Wrap(err, "failed to generate man (troff) manual")
}
fmt.Println("Troff (man markup) manual doc created in: ", manOutDir)
}
return nil
}

17
cmd/doc/main.go Normal file
View File

@ -0,0 +1,17 @@
package doc
import (
"github.com/spf13/cobra"
)
func NewDocCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "doc",
Short: "Documentation generation internal utilities",
Hidden: true, // Hide from vet public commands and docs itself, since its only build utility
}
cmd.AddCommand(newGenerateCommand())
return cmd
}

View File

@ -9,21 +9,34 @@ import (
"buf.build/gen/go/safedep/api/grpc/go/safedep/services/malysis/v1/malysisv1grpc"
malysisv1pb "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/malysis/v1"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
malysisv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/malysis/v1"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/safedep/dry/adapters"
"github.com/safedep/dry/api/pb"
"github.com/safedep/dry/utils"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/analytics"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/common/registry"
vetutils "github.com/safedep/vet/pkg/common/utils"
"github.com/safedep/vet/pkg/malysis"
"github.com/spf13/cobra"
"github.com/safedep/vet/pkg/reporter"
)
var (
malwareAnalysisPackageUrl string
malwareAnalysisTimeout time.Duration
malwareAnalysisReportJSON string
malwareAnalysisReportOSV string
malwareAnalysisNoWait bool
malwareReportOSVFinderName string
malwareReportOSVContacts []string
malwareReportOSVReferenceURL string
malwareReportOSVUseRange bool
)
func newPackageMalwareInspectCommand() *cobra.Command {
@ -47,6 +60,18 @@ func newPackageMalwareInspectCommand() *cobra.Command {
"Timeout for malware analysis")
cmd.Flags().StringVar(&malwareAnalysisReportJSON, "report-json", "",
"Path to save malware analysis report in JSON format")
cmd.Flags().StringVar(&malwareAnalysisReportOSV, "report-osv", "",
"Dir path to save malware analysis report in OSV format and ossf/malicious-packages format")
cmd.Flags().BoolVar(&malwareAnalysisNoWait, "no-wait", false,
"Do not wait for malware analysis to complete")
cmd.Flags().StringVar(&malwareReportOSVFinderName, "report-osv-finder-name", "",
"Finder name for malware analysis report in OSV format")
cmd.Flags().StringSliceVar(&malwareReportOSVContacts, "report-osv-contacts", []string{},
"Contacts for malware analysis report in OSV format (URL, email, etc.)")
cmd.Flags().StringVar(&malwareReportOSVReferenceURL, "report-osv-reference-url", "",
"Custom reference URL for malware analysis report (defaults to app.safedep.io)")
cmd.Flags().BoolVar(&malwareReportOSVUseRange, "report-osv-with-ranges", false,
"Use range-based versioning in OSV report (default: use explicit versions)")
_ = cmd.MarkFlagRequired("purl")
@ -54,6 +79,14 @@ func newPackageMalwareInspectCommand() *cobra.Command {
}
func executeMalwareAnalysis() error {
analytics.TrackCommandInspectMalwareAnalysis()
err := auth.Verify()
if err != nil {
return fmt.Errorf("access to Malicious Package Analysis requires an API key. " +
"For more details: https://docs.safedep.io/cloud/quickstart/")
}
cc, err := auth.MalwareAnalysisClientConnection("malware-analysis")
if err != nil {
return err
@ -66,17 +99,54 @@ func executeMalwareAnalysis() error {
return err
}
githubClient, err := adapters.NewGithubClient(adapters.DefaultGitHubClientConfig())
if err != nil {
return fmt.Errorf("failed to create GitHub client: %v", err)
}
versionResolver, err := registry.NewPackageVersionResolver(githubClient)
if err != nil {
return fmt.Errorf("failed to create package version resolver: %v", err)
}
packageVersion := purl.PackageVersion()
// If package version is empty or latest replace it with actual literal latest version
// Reference: https://github.com/safedep/vet/issues/446
if packageVersion.GetVersion() == "" || packageVersion.GetVersion() == "latest" {
ui.PrintMsg("Resolving package version")
version, err := versionResolver.ResolvePackageLatestVersion(purl.Ecosystem(), purl.Name())
if err != nil {
return fmt.Errorf("failed to resolve package latest version: %v", err)
}
ui.PrintSuccess("Resolved package version: %s", version)
packageVersion.Version = version
}
ctx := context.Background()
ctx, cancelFun := context.WithTimeout(ctx, malwareAnalysisTimeout)
defer cancelFun()
// For GitHub Actions packages, we need to resolve the commit hash
if packageVersion.GetPackage().GetEcosystem() == packagev1.Ecosystem_ECOSYSTEM_GITHUB_ACTIONS {
ui.PrintMsg("Resolving commit hash for GitHub Actions package")
commitHash, err := resolveGitHubActionsCommitHash(ctx, packageVersion)
if err != nil {
return fmt.Errorf("failed to resolve commit hash for GitHub Actions package: %v", err)
}
ui.PrintSuccess("Resolved commit hash for GitHub Actions package: %s", commitHash)
packageVersion.Version = commitHash
}
analyzePackageResponse, err := service.AnalyzePackage(ctx, &malysisv1.AnalyzePackageRequest{
Target: &malysisv1pb.PackageAnalysisTarget{
PackageVersion: purl.PackageVersion(),
PackageVersion: packageVersion,
},
})
if err != nil {
return fmt.Errorf("failed to submit package for malware analysis: %v", err)
}
@ -84,14 +154,18 @@ func executeMalwareAnalysis() error {
ui.PrintMsg("Submitted package for malware analysis with ID: %s",
analyzePackageResponse.GetAnalysisId())
if malwareAnalysisNoWait {
return nil
}
ui.StartSpinner("Waiting for malware analysis to complete")
var report *malysisv1pb.Report
var verificationRecord *malysisv1pb.VerificationRecord
for {
reportResponse, err := service.GetAnalysisReport(ctx, &malysisv1.GetAnalysisReportRequest{
AnalysisId: analyzePackageResponse.GetAnalysisId(),
})
if err != nil {
return fmt.Errorf("failed to get malware analysis report: %v", err)
}
@ -102,6 +176,7 @@ func executeMalwareAnalysis() error {
if reportResponse.GetStatus() == malysisv1.AnalysisStatus_ANALYSIS_STATUS_COMPLETED {
report = reportResponse.GetReport()
verificationRecord = reportResponse.GetVerificationRecord()
break
}
@ -116,29 +191,71 @@ func executeMalwareAnalysis() error {
ui.PrintSuccess("Malware analysis completed successfully")
err = renderToJSON(report)
if malwareAnalysisReportJSON != "" {
ui.PrintMsg("Generating JSON report")
err = writeJSONReport(report)
if err != nil {
ui.PrintError("Failed to render malware analysis report in JSON format: %v", err)
}
return renderMalwareAnalysisReport(malwareAnalysisPackageUrl,
analyzePackageResponse.GetAnalysisId(), report)
}
func renderToJSON(report *malysisv1pb.Report) error {
if malwareAnalysisReportJSON == "" {
if malwareAnalysisReportOSV != "" {
if !report.GetInference().GetIsMalware() {
ui.PrintWarning("Report is not malware, skipping OSV report generation")
return nil
} else {
ui.PrintMsg("Generating OSV report in: %s", malwareAnalysisReportOSV)
err = writeOSVReport(report)
if err != nil {
ui.PrintError("Failed to render malware analysis report in OSV format: %v", err)
}
}
}
return renderMalwareAnalysisReport(malwareAnalysisPackageUrl,
analyzePackageResponse.GetAnalysisId(), report, verificationRecord)
}
func writeOSVReport(report *malysisv1pb.Report) error {
err := os.MkdirAll(malwareAnalysisReportOSV, 0o755)
if err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}
generator, err := malysis.NewOpenSSFMaliciousPackageReportGenerator(malysis.OpenSSFMaliciousPackageReportGeneratorConfig{
Dir: malwareAnalysisReportOSV,
})
if err != nil {
return fmt.Errorf("failed to create OpenSSF malicious package report generator: %v", err)
}
err = generator.GenerateReport(context.Background(), report, malysis.OpenSSFMaliciousPackageReportParams{
FinderName: malwareReportOSVFinderName,
Contacts: malwareReportOSVContacts,
ReferenceURL: malwareReportOSVReferenceURL,
UseRange: malwareReportOSVUseRange,
})
if err != nil {
return fmt.Errorf("failed to generate OpenSSF malicious package report: %v", err)
}
return nil
}
func writeJSONReport(report *malysisv1pb.Report) error {
data, err := utils.ToPbJson(report, " ")
if err != nil {
return err
}
return os.WriteFile(malwareAnalysisReportJSON, []byte(data), 0644)
return os.WriteFile(malwareAnalysisReportJSON, []byte(data), 0o644)
}
func renderMalwareAnalysisReport(purl string, analysisId string, report *malysisv1pb.Report) error {
func renderMalwareAnalysisReport(purl string, analysisId string,
report *malysisv1pb.Report, vr *malysisv1pb.VerificationRecord,
) error {
ui.PrintMsg("Malware analysis report for package: %s", purl)
tbl := table.NewWriter()
@ -147,9 +264,13 @@ func renderMalwareAnalysisReport(purl string, analysisId string, report *malysis
tbl.AppendHeader(table.Row{"Package URL", "Status", "Confidence"})
status := text.FgHiGreen.Sprint("SAFE")
status := reporter.InfoBgText(" SAFE ")
if report.GetInference().GetIsMalware() {
status = text.FgHiRed.Sprint("MALWARE")
if vr != nil && vr.IsMalware {
status = reporter.CriticalBgText(" MALICIOUS ")
} else {
status = reporter.WarningBgText(" SUSPICIOUS ")
}
}
confidence := report.GetInference().GetConfidence().String()
@ -159,8 +280,8 @@ func renderMalwareAnalysisReport(purl string, analysisId string, report *malysis
tbl.Render()
fmt.Println()
fmt.Println(text.FgHiYellow.Sprintf("** The full report is available at: %s",
reportVisualizationUrl(analysisId)))
fmt.Println(reporter.WarningText(fmt.Sprintf("** The full report is available at: %s",
reportVisualizationUrl(analysisId))))
fmt.Println()
return nil
@ -169,3 +290,21 @@ func renderMalwareAnalysisReport(purl string, analysisId string, report *malysis
func reportVisualizationUrl(analysisId string) string {
return malysis.ReportURL(analysisId)
}
func resolveGitHubActionsCommitHash(ctx context.Context, packageVersion *packagev1.PackageVersion) (string, error) {
gha, err := adapters.NewGithubClient(adapters.DefaultGitHubClientConfig())
if err != nil {
return "", fmt.Errorf("failed to create GitHub client: %v", err)
}
parts := strings.Split(packageVersion.GetPackage().GetName(), "/")
if len(parts) != 2 {
return "", fmt.Errorf("invalid repository name: %s - should be in the format <owner>/<repo>", packageVersion.GetPackage().GetName())
}
owner := parts[0]
repo := parts[1]
return vetutils.ResolveGitHubRepositoryCommitSHA(ctx,
gha, owner, repo, packageVersion.GetVersion())
}

17
cmd/server/main.go Normal file
View File

@ -0,0 +1,17 @@
package server
import "github.com/spf13/cobra"
func NewServerCommand() *cobra.Command {
cmd := cobra.Command{
Use: "server",
Short: "Start available servers",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.AddCommand(newMcpServerCommand())
return &cmd
}

195
cmd/server/mcp.go Normal file
View File

@ -0,0 +1,195 @@
package server
import (
"fmt"
"os"
"buf.build/gen/go/safedep/api/grpc/go/safedep/services/insights/v2/insightsv2grpc"
"buf.build/gen/go/safedep/api/grpc/go/safedep/services/malysis/v1/malysisv1grpc"
"github.com/safedep/dry/adapters"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/auth"
"github.com/safedep/vet/mcp"
"github.com/safedep/vet/mcp/server"
"github.com/safedep/vet/mcp/tools"
"github.com/safedep/vet/pkg/common/logger"
)
var (
mcpServerSseServerAddr string
mcpServerServerType string
skipDefaultTools bool
registerVetSQLQueryTool bool
vetSQLQueryToolDBPath string
registerPackageRegistryTool bool
sseServerAllowedOrigins []string
sseServerAllowedHosts []string
)
func newMcpServerCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "mcp",
Short: "Start the MCP server",
RunE: func(cmd *cobra.Command, args []string) error {
err := startMcpServer()
if err != nil {
logger.Errorf("Failed to start server: %v", err)
os.Exit(1)
}
return nil
},
}
cmd.Flags().StringVar(&mcpServerSseServerAddr, "sse-server-addr", "localhost:9988", "The address to listen for SSE connections")
cmd.Flags().StringVar(&mcpServerServerType, "server-type", "stdio", "The type of server to start (stdio, sse)")
cmd.Flags().StringSliceVar(
&sseServerAllowedOrigins,
"sse-allowed-origins",
nil,
"List of allowed origin prefixes for SSE connections. By default, we allow http://localhost:, http://127.0.0.1: and https://localhost:.",
)
cmd.Flags().StringSliceVar(
&sseServerAllowedHosts,
"sse-allowed-hosts",
nil,
"List of allowed hosts for SSE connections. By default, we allow localhost:9988, 127.0.0.1:9988 and [::1]:9988.",
)
// We allow skipping default tools to allow for custom tools to be registered when the server starts.
// This is useful for agents to avoid unnecessary tool registration.
cmd.Flags().BoolVar(&skipDefaultTools, "skip-default-tools", false, "Skip registering default tools")
// Options to register sqlite3 query tool
cmd.Flags().BoolVar(&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
}

60
cmd/server/mcp_test.go Normal file
View File

@ -0,0 +1,60 @@
package server
import (
"context"
"testing"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/stretchr/testify/assert"
"github.com/safedep/vet/test"
)
func TestMcpDriver(t *testing.T) {
test.EnsureEndToEndTestIsEnabled(t)
driver, err := buildMcpDriver()
if err != nil {
t.Fatalf("failed to build MCP driver: %v", err)
}
t.Run("malysis community service is accessible", func(t *testing.T) {
report, err := driver.GetPackageVersionMalwareReport(context.Background(), &packagev1.PackageVersion{
Package: &packagev1.Package{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
Name: "express",
},
Version: "4.17.1",
})
assert.NoError(t, err)
assert.NotNil(t, report)
})
t.Run("insights community service is accessible", func(t *testing.T) {
vulns, err := driver.GetPackageVersionVulnerabilities(context.Background(), &packagev1.PackageVersion{
Package: &packagev1.Package{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
Name: "express",
},
Version: "4.17.1",
})
assert.NoError(t, err)
assert.NotNil(t, vulns)
assert.NotEmpty(t, vulns)
})
t.Run("package registry adapter is accessible", func(t *testing.T) {
res, err := driver.GetPackageLatestVersion(context.Background(), &packagev1.Package{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
Name: "express",
})
assert.NoError(t, err)
assert.NotNil(t, res)
assert.Equal(t, "express", res.GetPackage().GetName())
assert.Equal(t, packagev1.Ecosystem_ECOSYSTEM_NPM, res.GetPackage().GetEcosystem())
assert.NotEmpty(t, res.GetPackage().GetName())
})
}

View File

@ -1,19 +1,19 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"context"
"net/http"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/cli/oauth/device"
"github.com/spf13/cobra"
"github.com/safedep/vet/internal/connect"
"github.com/safedep/vet/internal/ui"
"github.com/safedep/vet/pkg/common/logger"
"github.com/spf13/cobra"
)
func newConnectCommand() *cobra.Command {
@ -143,7 +143,6 @@ func connectGithubWithDeviceFlow() (string, error) {
ClientID: clientID,
DeviceCode: code,
})
if err != nil {
return "", err
}

View File

@ -1,33 +1,9 @@
# vet docs overview
# vet Documentation
This section contains documentation for `vet`. This documentation site is based on the Docusaurus framework.
## Usage
## Getting Started
`vet` user documentation is available at [https://docs.safedep.io/](https://docs.safedep.io/)
To start a local environment of this project docs, please do the following
## Development
- Clone the repository
```bash
git clone https://github.com/safedep/vet.git
```
- Navigate to the guide directory
```bash
cd vet/docs
```
- Install dependencies
```bash
npm install
```
- Start the development server
```bash
npm start
```
- Navigate to [http://localhost:3000](http://localhost:3000) for accessing the `vet` documentation locally
- [Storage](./storage.md)

40
docs/agent.md Normal file
View File

@ -0,0 +1,40 @@
# Agents
`vet` natively supports AI agents with MCP based integration for tools.
To get started, set an API key for the LLM you want to use. Example:
```bash
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-...
export GEMINI_API_KEY=AIza...
```
> **Note:** You can also set the model to use with `OPENAI_MODEL_OVERRIDE`, `ANTHROPIC_MODEL_OVERRIDE` and `GEMINI_MODEL_OVERRIDE` environment variables to override the default model used by the agent.
## Fast Mode
All agents support a `--fast` flag to use a faster LLM model instead of a slower but more powerful reasoning model. This is only for influencing the default choice of model. It can be overridden by setting model provider specific environment variables such as `OPENAI_MODEL_OVERRIDE`, `ANTHROPIC_MODEL_OVERRIDE` and `GEMINI_MODEL_OVERRIDE`.
## Available Agents
### Query Agent
The query agent helps run query and analysis over vet's sqlite3 reporting database. To use it:
* Run a `vet` scan and generate report in sqlite3 format
```bash
vet scan --insights-v2 -M package-lock.json --report-sqlite3 report.db
```
**Note:** Agents only work with `--insights-v2`
* Start the query agent
```bash
vet agent query --db report.db
```
* Thats it! Start asking questions about the scan results.

View File

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

BIN
docs/assets/vet-demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

View File

Before

Width:  |  Height:  |  Size: 489 KiB

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

View File

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

View File

@ -1,3 +0,0 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

35
docs/doc-generate.md Normal file
View File

@ -0,0 +1,35 @@
# Doc Generate
Docs for `cmd/doc/` command
> [!NOTE]
> This command is `HIDDEN` and not listed in the help output of `vet`. It is used to generate the documentation / manual for the `vet` command line application.
## doc
Documentation generation internal utilities
### Options
```
-h, --help help for doc
```
## doc generate
Generate docs / manual artifacts
```
doc generate [flags]
```
### Options
```
-h, --help help for generate
--man string The output directory for troff (man markup) doc files
--markdown string The output directory for markdown doc files
```
> [!IMPORTANT]
> At least one of the output directory is required

View File

@ -1,8 +0,0 @@
{
"label": "👀 Advanced Usage",
"position": 6,
"link": {
"type": "generated-index",
"description": "Advanced usage of vet for various workflows"
}
}

View File

@ -1,9 +0,0 @@
---
sidebar_position: 6
title: 🚫 Allow & Deny List
draft: true
---
# 🚫 Vet - Allow & Deny List
In this section we will leverage the [Exceptions](./exceptions) to configure and design the entire workflows with allow & deny list.

View File

@ -1,43 +0,0 @@
---
sidebar_position: 2
title: ✍️ Build Your Own Queries
---
# ✍️ Build Your Own Queries (BYOQ)
## Query Workflow
Scanning a package manifest is a resource intensive process as it involves enriching package metadata by querying Insights API. However, filtering and reporting may be done multiple times on the same manifest. To speed up the process, we can dump the enriched data as JSON and load the same for filtering and reporting.
- Dump enriched JSON manifests to a directory (example)
```bash
vet scan --lockfile /path/to/package-lock.json --json-dump-dir /tmp/dump
vet scan -D /path/to/repository --json-dump-dir /tmp/dump-many
```
- Load the enriched metadata for filtering and reporting
```bash
vet query --from /tmp/dump --report-summary
vet query --from /tmp/dump --filter 'scorecard.score.Maintained == 0'
```
## Security Guardrails with Filters
A simple security guardrail (in CI) can be achieved using the filters. The `--filter-fail` argument tells the `Filter Analyzer` module to fail the command if any package matches the given filter.
- Example: If OpenSSF Scorecard not maintained project score is `0` then fail the build
```bash
vet query --from /path/to/json-dump \
--filter 'scorecard.scores.Maintained == 0' \
--filter-fail
```
- Subsequently, the command fails with `-1` exit code in case of match
```bash
➜ vet git:(develop) ✗ echo $?
255
```

View File

@ -1,88 +0,0 @@
---
sidebar_position: 4
title: ❗ Exceptions
---
# ❗ Vet - Exceptions Management
Any security scanning tool may produce
1. False positive
2. Issues that are acceptable for a period of time
3. Issues that are ignored permanently
:::info
To support exceptions, we introduce the exception model defined in [exception spec](https://github.com/safedep/vet/blob/main/api/exceptions_spec.proto)
:::
## Use-case
As a user of `vet` tool, I want to add all existing packages or package versions as `exceptions` to make the scanner and filter analyses to ignore them while reporting issues so that I can deploy `vet` as a security guardrail to prevent introducing new packages with security issues
This workflow will allow users to
1. Accept the current issues as backlog to be mitigated over time
2. Deploy `vet` as a security gate in CI to prevent introducing new issues
### Security Risks
Exceptions management should handle the potential security risk of ignoring a package and its future issues. To mitigate this risk, we will ensure that issues can be ignored till an acceptable time window and not permanently.
## Workflow
### Generate Exceptions File
- Run a scan and dump raw data to a directory
```bash
vet scan -D /path/to/repo --json-dump-dir /path/to/dump
```
- Use `vet query` to generate exceptions for all existing packages
```bash
vet query --from /path/to/dump \
--exceptions-generate /path/to/exceptions.yml \
--exceptions-filter 'true' \ # Optional filter for packages to add
--exceptions-till '2023-12-12'
```
:::info
`--exceptions-till` is parsed as `YYYY-mm-dd` and will generate a timestamp of `00:00:00` in UTC timezone for the date in RFC3339 format
:::
### Customize Exceptions File
The generated exceptions file will add all packages, matching optional filter, into the `exceptions.yml` file. This file should be reviewed and customised as required before using it.
### Use Exceptions to Ignore Specific Packages
An exceptions file can be passed as a global flag to `vet`. It will be used for various commands such as `scan` or `query`.
```bash
./vet --exceptions /path/to/exceptions.yml scan -D /path/to/repo
```
:::caution
Do not pass this flag while generating exceptions list in query workflow to avoid incorrect exception list generation
:::
## Behavior
- All exceptions rules are applied only on a `Package`
- All comparisons will be case-insensitive except version
- Only `version` can have a value of `*` matching any version
- Exceptions are globally managed and will be shared across packages
- Exempted packages will be ignored by all analysers and reporters
- First match policy for exceptions matching
Anti-patterns that will NOT be implemented
- Exceptions will not be implemented for manifests because they will cause false negatives
- Exceptions will not be created without an expiry to avoid future false negatives on the package

View File

@ -1,125 +0,0 @@
---
sidebar_position: 1
title: 🔎 Filtering
---
# 🔎 Filtering
Filter command helps solve the problem of visibility for OSS dependencies in an application. To support various requirements, we adopt a generic [expressions language](https://github.com/google/cel-spec) for flexible filtering.
## Example
- The scan will list only packages that use the `MIT` license.
```bash
vet scan -D /path/to/repo \
--report-summary=false \
--filter 'licenses.exists(p, p == "MIT")'
```
- Find dependencies that seems not very popular
```bash
vet scan --lockfiles /path/to/pom.xml --report-summary=false \
--filter='projects.exists(x, x.stars < 10)'
```
- Find dependencies with a critical vulnerability
```bash
vet scan --lockfiles /path/to/pom.xml --report-summary=false \
--filter='vulns.critical.exists_one(x, true)'
```
## Input
Filter expressions work on packages (aka. dependencies) and evaluates to a boolean result. The package is included in the results table if the expression evaluates to `true`.
- Filter expressions get the following input data to work with
| Variable | Content |
|-------------|-------------------------------------------------------------|
| `_` | The root variable, holding other variables |
| `vulns` | Holds a map of vulnerabiliteis by severity |
| `scorecard` | Holds OpenSSF scorecard |
| `projects` | Holds a list of source projects associated with the package |
| `licenses` | Holds a list of liceses in SPDX license code format |
:::tip
Refer to [filter input spec](https://github.com/safedep/vet/blob/main/api/filter_input_spec.proto) for detailed structure of input messages.
:::
## Expressions
Expressions are [CEL](https://github.com/google/cel-spec) statements. While
CEL internals are not required, an [introductory](https://github.com/google/cel-spec/blob/master/doc/intro.md)
knowledge of CEL will help formulating queries. Expressions are logical
statements that evaluate to `true` or `false`.
### Example Queries
| Description | Query |
|----------------------------------------------|--------------------------------------|
| Find packages with a critical vulnerability | `vulns.critical.exists(x, true)` |
| Find unmaintained packages as per OpenSSF SC | `scorecard.scores.Maintained == 0` |
| Find packages with low stars | `projects.exists(x, x.stars < 10)` |
| Find packages with GPL-2.0 license | `licenses.exists(x, x == "GPL-2.0")`
:::tip
Refer to [scorecard checks](https://github.com/ossf/scorecard#checks-1) for a list of checks available from OpenSSF Scorecards project.
:::
## How does the filter input JSON look like?
```json
{
"pkg": {
"ecosystem": "npm",
"name": "lodash.camelcase",
"version": "4.3.0"
},
"vulns": {
"all": [],
"critical": [],
"high": [],
"medium": [],
"low": []
},
"scorecard": {
"scores": {
"Binary-Artifacts": 10,
"Branch-Protection": 0,
"CII-Best-Practices": 0,
"Code-Review": 8,
"Dangerous-Workflow": 10,
"Dependency-Update-Tool": 0,
"Fuzzing": 0,
"License": 10,
"Maintained": 0,
"Packaging": -1,
"Pinned-Dependencies": 9,
"SAST": 0,
"Security-Policy": 10,
"Signed-Releases": -1,
"Token-Permissions": 0,
"Vulnerabilities": 10
}
},
"projects": [
{
"name": "lodash/lodash",
"type": "GITHUB",
"stars": 55518,
"forks": 6787,
"issues": 464
}
],
"licenses": [
"MIT"
]
}
```

View File

@ -1,27 +0,0 @@
---
sidebar_position: 6
title: 🚧 Path Exclusion
---
# 🚧 Path Exclusion
`vet` supports path exclusions for scenarios where a directory is the scan target but certain path patterns within the directory should be excluded from scan. This is available only for the `scan` command.
```bash
vet scan -D /path/to/target --exclude 'docs/*'
```
- Multiple path patterns can be provided for exclusion while scanning a directory
```bash
vet scan -D /path/to/target \
--exclude 'docs/*' \
--exclude 'sub/dir/path/*'
```
:::info
The exclusion pattern matches any path, directory or file. Internally it uses
Go regexp [MatchString](https://pkg.go.dev/regexp#MatchString)
:::

View File

@ -1,66 +0,0 @@
---
sidebar_position: 3
title: 📜 Policy as Code
---
# 📜 Policy as Code (PaC)
## What is Policy as Code?
The idea of treating policy as code / config / rules that can be evaluated by tools to make runtime decisions. Generally, policy as code approach reduces ***cost of compliance*** by automating grunt work required to audit for policy violations.
## Why use Policy as Code?
Manual verification and approval of new software components is, to put it simply, tedious, contentious, error prone and incomplete. It is not possible to manually analyses a 3rd party dependency and its transitive dependencies. We believe organizational software supply chain policies should be codified so that tools can be build to automatically evaluate every 3rd party artifact for compliance against policies.
Examples of software supply chain policy may include:
- Dependencies must not have known critical vulnerabilities
- Dependencies must be maintained
- Dependencies must not be new / must have an extensive user base
- Publishers must follow security standards e.g. [SLSA](https://slsa.dev/)
## Filter Suite
A single filter is useful for identification of packages that meet some specific criteria. While it helps solve various use-cases, it is not entirely suitable for `security guardrails` where multiple filters may be required to express an organization's acceptable OSS usage policy.
- For example, an organization may define a filter to deny certain type of packages
1. Any package that has a high or critical vulnerability
2. Any package that does not match acceptable OSS licenses
3. Any package that has a low [OpenSSF scorecard score](https://github.com/ossf/scorecard)
- To express this policy, multiple filters are needed such as
```bash
vulns.critical.exists(p, true) ||
licenses.exists(p, (p != "MIT") && (p != "Apache-2.0")) ||
(scorecard.scores.Maintained == 0)
```
- To solve this problem, we introduce the concept of `Filter Suite`. It can be represented as an YAML file containing multiple filters to match:
```yaml
name: Generic Filter Suite
description: Example filter suite with canned filters
filters:
- name: critical-vuln
value: |
vulns.critical.exists(p, true)
- name: safe-licenses
value: |
licenses.exists(p, (p != "MIT") && (p != "Apache-2.0"))
- name: ossf-maintained
value: |
scorecard.scores.Maintained == 0
```
- A scan or query operation can be invoked using the filter suite:
```bash
vet scan -D /path/to/repo --filter-suite /path/to/filters.yml --filter-fail
```
- The filter suite will be evaluated as
- Ordered list of filters as given in the suite file
- Stop on first rule match for a given package

View File

@ -1,65 +0,0 @@
---
sidebar_position: 9
title: ⏩ Shell Completion
---
# ⏩ Shell completion for vet
Command-line completion or Shell completion is a feature provided by shells like `bash` or `zsh` that lets you type commands in a fast and easy way. This functionality automatically fills in partially typed commands when the user press the `tab` key.
- To enable shell completion for `vet` for various shells follow the below steps
## 1. Identify your current environment shell
```bash
echo $SHELL
/bin/zsh
```
## 2. Generate the completion command for your shell
```zsh
vet completion zsh -h
Generate the autocompletion script for the zsh shell.
If shell completion is not already enabled in your environment you will need
to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions in your current shell session:
source <(vet completion zsh); compdef _vet vet
To load completions for every new session, execute once:
#### Linux:
vet completion zsh > "${fpath[1]}/_vet"
#### macOS:
vet completion zsh > $(brew --prefix)/share/zsh/site-functions/_vet
You will need to start a new shell for this setup to take effect.
```
## 3. Run the commands to setup completion
```zsh
echo "autoload -U compinit; compinit" >> ~/.zshrc
source <(vet completion zsh); compdef _vet vet
vet completion zsh > $(brew --prefix)/share/zsh/site-functions/_vet
```
## 4. Open new shell and you can see the completion activated
```zsh
vet [tab]
```
![vet autocomplete](/img/vet/vet-autocomplete.png)

View File

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

View File

@ -1,12 +0,0 @@
---
sidebar_position: 17
title: 🎊 Community
---
# 🎊 Community
First of all, thank you so much for showing interest in `vet`, we appreciate it ❤️
**Join the server using the link - https://rebrand.ly/safedep-community**
[![SafeDep Discord](/img/safedep-discord.png)](https://rebrand.ly/safedep-community)

View File

@ -1,136 +0,0 @@
---
sidebar_position: 4
title: 🧩 Configuration
draft: true
---
# 🧩 Configuring Vet
`vet` comes with super powers 🚀, this section will help you to understand and explore some of them so that you can take your open source security to next level 😎
## Scanning
### Scanning Directories
- If you wanted to scan the whole directory & automatically parse the dependencies/lockfile, you can use the `-D` or `--directory` flag.
```bash
vet scan -D your-code/directory/path/
```
:::info
If you do not specify any directory, by default it takes present working directory as the input.
:::
### Scanning Files
- If you wanted to scan the specific file `lockfile` you can use the `-L` or `--lockfiles` flag.
```bash
vet scan -D your-code/directory/path/
```
:::info
If you do not specify any directory, by default it takes present working directory as the input.
:::
### Scanning Non-standard files
- Sometimes you might have non-standard filenames for the dependencies, lockfiles. You can scan them as a supported package manifest with a non-standard name using the following command
```bash
vet scan --lockfiles /path/to/gradle-compileOnly.lock --lockfile-as gradle.lockfile
```
### Scanning Multiple files
```bash
vet scan --lockfiles /path/to/gradle.lockfile --lockfiles requirements.txt
```
![vet scanning multiple files](/img/vet/scanning-multiple-files.png)
### Scanning Parsers
`vet` currently has 10 scanning parsers for various dependencies formats including Go, Python, Java, etc.
```bash
vet scan parsers
Available Lockfile Parsers
==========================
[0] buildscript-gradle.lockfile
[1] go.mod
[2] gradle.lockfile
[3] package-lock.json
[4] Pipfile.lock
[5] pnpm-lock.yaml
[6] poetry.lock
[7] pom.xml
[8] requirements.txt
[9] yarn.lock
```
## Scan Options
### Silent scan
- `vet` supports silent scan to prevent rendering UI using the following command with `-s` or `--silent` flag
```bash
vet scan -s --lockfiles demo-client-java/gradle.lockfile
```
![vet silent scan](/img/vet/silent-scan.png)
### Scan concurrency
- By default it set to `5`, you can increase or decrease using the `--concurrency` or `-C` flag
```bash
vet scan -C 10 --lockfiles demo-client-java/gradle.lockfile
Scanning packages ... done! [115 in 5.87s]
Scanning manifests ... done! [1 in 5.87s]
```
- You can see the difference between the above and below scan time with same file(s)
```bash
vet scan -C 1 --lockfiles demo-client-java/gradle.lockfile
Scanning packages ... done! [115 in 10.567s]
Scanning manifests ... done! [1 in 10.567s]
```
### Scanning transitive dependencies
- You can perform the transitive dependencies scan by running the following command with `--transitive` flag
```bash
vet scan --transitive --lockfiles demo-client-java/gradle.lockfile
```
![vet transitive scan default](/img/vet/vet-transitive-default.png)
- As you can see the above scan has found issues across `201` libraries
### Configuring transitive dependencies depth level
- You can change the transitive dependencies scan depth by running the following command with `--transitive-depth` flag
```bash
vet scan --transitive --transitive-depth 5 --lockfiles demo-client-java/gradle.lockfile
```
![vet transitive scan depth](/img/vet/vet-transitive-depth.png)
- As you can see the above scan has found issues across `237` libraries
:::info
By default if you don't specify the flag it takes `2` as depth
:::

View File

@ -1,9 +0,0 @@
{
"label": "🌐 Ecosystem",
"position": 15,
"link": {
"type": "generated-index",
"description": "🚧 Work-in-Progress (WIP): SafeDep's vet ecosystem"
}
}

View File

@ -1,11 +0,0 @@
---
draft: true
---
# Artifactory Systems
🚧 Work-in-Progress (WIP)
## JFrog
## Nexus

View File

@ -1,7 +0,0 @@
---
draft: true
---
# Developer Tooling
🚧 Work-in-Progress (WIP)

View File

@ -1,14 +0,0 @@
---
draft: true
---
# Gateways
🚧 Work-in-Progress (WIP)
## Kong API Gateway
## AWS API Gateway
## Traefik Gateway

View File

@ -1,15 +0,0 @@
---
draft: true
---
# IDE
🚧 Work-in-Progress (WIP)
## VSCode
## JetBrains
## Vim
## NeoVim

View File

@ -1,13 +0,0 @@
---
draft: true
---
# Integrations
🚧 Work-in-Progress (WIP)
## GitHub Actions
## Gitlab CI
## Bitbucket Pipelines

View File

@ -1,13 +0,0 @@
---
draft: true
---
# Logging & Monitoring Systems
🚧 Work-in-Progress (WIP)
## Elastic Stack
## Splunk
## DataDog

View File

@ -1,15 +0,0 @@
---
draft: true
---
# Service Mesh
🚧 Work-in-Progress (WIP)
## Istio - Service Mesh
## LinkerD - Service Mesh
## Cilium - Service Mesh
## Kong - Service Mesh

View File

@ -1,15 +0,0 @@
---
draft: true
---
# Systems & Solutions
🚧 Work-in-Progress (WIP)
## Kubernetes
## Nomad
## AWS Fargate
## Google Cloud Run

View File

@ -1,26 +0,0 @@
---
sidebar_position: 20
title: 🙋 FAQ
---
# 🙋 FAQ - Vet
### How do I disable the stupid banner?
- Set environment variable `VET_DISABLE_BANNER=1`
### Something is wrong! How do I debug this thing?
- Run without the eye candy UI and enable log to file or to `stdout`.
Log to `stdout`:
```bash
vet scan -D /path/to/repo -s -l- -v
```
Log to file:
```bash
vet scan -D /path/to/repo -l /tmp/vet.log -v
```

View File

@ -1,8 +0,0 @@
{
"label": "📖 Guides",
"position": 8,
"link": {
"type": "generated-index",
"description": "Guides for various use-cases"
}
}

View File

@ -1,87 +0,0 @@
---
sidebar_position: 2
title: 🏄 Code Analysis
---
# Code Analysis
:::note
EXPERIMENTAL: This feature is experimental and may introduce breaking changes.
:::
`vet` has a code analysis framework built on top of [tree-sitter](https://tree-sitter.github.io/tree-sitter/) parsers. The goal
of this framework is to support multiple languages, source repositories (local and remote),
and create a representation of code that can be analysed for common software
supply chain security related use-cases such as
- Identify shadowed imports
- Identify evidence of a dependency actually being used
- Import reachability analysis
- Function reachability analysis
:::warning
The code analysis framework is designed specifically to be simple, fast and
not to be a full-fledged static analysis tool. It is currently in early stages
of development and may not support all languages or maintain API compatibility.
:::
## Build a Code Analysis Database
- Analyse code and build a database for further analysis.
```bash
vet code --db /tmp/code.db \
--src /path/to/app \
--imports /virtualenvs/app/lib/python3.11/site-packages \
--lang python \
create-db
```
The above command does the following:
- Uses Python as the language for parsing source code
- Analyses application code recursively in `/path/to/app`
- Analyses dependencies in `/virtualenvs/app/lib/python3.11/site-packages`
- Creates a database at `/tmp/code.db` for further analysis
## Manual Query Execution
Use [cayleygraph](https://cayley.gitbook.io/cayley/) to query the database.
```bash
docker run -it -p 64210:64210 -v /tmp/code.db:/db cayleygraph/cayley -a /db -d bolt
```
- Navigate to `http://127.0.0.1:64210` in your browser
### Query Examples
#### Dependency Graph
Build dependency graph for your application
```js
g.V().Tag("source").out("imports").Tag("target").all()
```
![Dependency Graph](/img/vet-code-demo-import-graph.png)
#### Import Reachability
Check if a specific import is reachable in your application
```js
g.V("app").followRecursive(g.M().out("imports")).is("six").all()
```
- `app` is the application originating from `app.py`
- `six` is a python module imported transitively
### Query API
Refer to [Gizmo Query Language](https://cayley.gitbook.io/cayley/query-languages/gizmoapi)
for documentation on constructing custom queries.

View File

@ -1,5 +0,0 @@
---
draft: true
---
# Dependency Cost

View File

@ -1,71 +0,0 @@
---
draft: false
title: 📦 Dependency Inventory
---
# Dependency Inventory
In this guide, we will use CycloneDX `gradle` plugin to generate a software
bill of material (SBOM) and scan it using `vet`.
## CycloneDX Plugin Integration
An official [plugin](https://github.com/CycloneDX/cyclonedx-gradle-plugin) can
be used with build automation tools such as Gradle, Maven, etc. to generate
Software Bill of Materials(SBOM) for a Java/Android/Kotlin projects.
### Gradle Plugin Integration
The gradle plugin for generating cyclonedx sbom file has to be integrated into
the build script i.e. `build.gradle` file.
```groovy
plugins {
id 'org.cyclonedx.bom' version '1.10.0'
}
cyclonedxBom {
includeConfigs = ["runtimeClasspath"]
skipConfigs = ["compileClasspath", "testCompileClasspath"]
skipProjects = [rootProject.name, "yourTestSubProject"]
projectType = "application"
schemaVersion = "1.6"
destination = file("build/reports")
outputName = "bom"
outputFormat = "json"
includeBomSerialNumber = false
includeLicenseText = false
includeMetadataResolution = true
componentVersion = "2.0.0"
componentName = "my-component"
}
```
Based on requirements, `includeConfigs` and `skipConfigs` properties in
`cyclonedxBom` can be modified to only include runtime, compile-time, or
implementation dependencies in the sbom artifact(s). Additionaly, in
a multi-build project, `skipProjects` property can be used to exclude
dependency resolution for a sub-project, thus reducing the noise.
### SBOM Generation
Now, to generate sbom artifacts, do a clean build of the project using its
respective build tool: `gradle cleanBuild -b build.gradle :cyclonedxBom`
![sample gradle build](../../static/img/sample-gradle-build.png)
After a successful build, all the artifacts shall be stored in `build/reports`
path, present in the project root.
## Scan SBOMs using Vet
vet supports scanning of SBOM files in both SPDX and CycloneDX format.
Depending upon the plugin and build tool being used, appropriate parsers can be
used to scan the artifacts for a vulnerability report.
```
vet scan --lockfiles build/reports/bom.json --lockfile-as bom-cyclonedx --report-markdown=report.md
vet scan --lockfiles build/reports/bom.json --lockfile-as bom-spdx --report-markdown=report.md
```
![vet cyclonedx scan demo](../../static/img/vet-cyclonedx-scan-demo.png)

View File

@ -1,5 +0,0 @@
---
draft: true
---
# Dependency Scanning

View File

@ -1,5 +0,0 @@
---
draft: true
---
# Drift Analysis

View File

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

View File

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

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