From 48610380cd6a0c385ed25817064de8eeda4aa682 Mon Sep 17 00:00:00 2001 From: Neeladri Das Date: Fri, 20 Mar 2026 07:33:27 +0000 Subject: [PATCH 1/4] jh scan command --- CLAUDE.md | 23 ++++- README.md | 31 +++++++ main.go | 60 +++++++++++++- scan.go | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 scan.go diff --git a/CLAUDE.md b/CLAUDE.md index 81231b1..56d248f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview -This is a Go-based CLI tool for interacting with JuliaHub, a platform for Julia computing. The CLI provides commands for authentication, dataset management, registry management, project management, user information, token management, Git integration, and Julia integration. +This is a Go-based CLI tool for interacting with JuliaHub, a platform for Julia computing. The CLI provides commands for authentication, dataset management, registry management, project management, user information, token management, vulnerability scanning, Git integration, and Julia integration. ## Architecture @@ -18,6 +18,7 @@ The application follows a command-line interface pattern using the Cobra library - **user.go**: User information retrieval using GraphQL API and REST API for listing users - **tokens.go**: Token management operations (list) with REST API integration - **landing.go**: Landing page management (show, update, remove) with REST API integration +- **scan.go**: Vulnerability scanning for Julia packages via REST API - **git.go**: Git integration (clone, push, fetch, pull) with JuliaHub authentication - **julia.go**: Julia installation and management - **run.go**: Julia execution with JuliaHub configuration @@ -32,7 +33,7 @@ The application follows a command-line interface pattern using the Cobra library - Stores tokens securely in `~/.juliahub` with 0600 permissions 2. **API Integration**: - - **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`), registry operations (`/api/v1/registry/registries/descriptions`, `/api/v1/registry/config/registry/{name}`), package search/info primary path (`/packages/info`), token management (`/app/token/activelist`), user management (`/app/config/features/manage`), and landing page management (`/app/homepage` GET, `/app/config/homepage` POST/DELETE) + - **REST API**: Used for dataset operations (`/api/v1/datasets`, `/datasets/{uuid}/url/{version}`), registry operations (`/api/v1/registry/registries/descriptions`, `/api/v1/registry/config/registry/{name}`), package search/info primary path (`/packages/info`), token management (`/app/token/activelist`), user management (`/app/config/features/manage`), landing page management (`/app/homepage` GET, `/app/config/homepage` POST/DELETE), and vulnerability scanning (`/api/v1/ui/vulnerabilities/packages/{name}`) - **GraphQL API**: Used for projects, user info, and package search/info fallback (`/v1/graphql`) - **Headers**: All GraphQL requests require `X-Hasura-Role: jhuser` header - **Authentication**: Uses ID tokens (`token.IDToken`) for API calls @@ -50,6 +51,7 @@ The application follows a command-line interface pattern using the Cobra library - `jh admin user`: User management (list all users with REST API, supports verbose mode) - `jh admin token`: Token management (list all tokens with REST API, supports verbose mode) - `jh admin landing-page`: Landing page management (show/update/remove custom markdown landing page with REST API) + - `jh scan`: Vulnerability scanning for Julia packages (REST API; supports `--version` to check a specific version, `--advisory` to show which versions are affected by a specific advisory ID, and `--verbose` for full advisory details) - `jh clone`: Git clone with JuliaHub authentication and project name resolution - `jh push/fetch/pull`: Git operations with JuliaHub authentication - `jh git-credential`: Git credential helper for seamless authentication @@ -158,6 +160,16 @@ cat landing.md | go run . admin landing-page update go run . admin landing-page remove ``` +### Test vulnerability scan operations +```bash +go run . scan MbedTLS_jll +go run . scan MbedTLS_jll --version 2.28.1010+0 +go run . scan MbedTLS_jll --version 2.28.1010+0 --verbose +go run . scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz +go run . scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz --verbose +go run . scan SomePackage -s nightly.juliahub.dev +``` + ### Test Git operations ```bash go run . clone john/my-project # Clone from another user @@ -370,6 +382,13 @@ jh run setup - GraphQL fallback uses `package_search_with_count.gql` which fetches both the package list and aggregate count in a single request (`package_search` + `package_search_aggregate` root fields) - `executeGraphQL(server, token, req)` in `packages.go` is a shared helper for GraphQL POST requests (sets Authorization, Content-Type, Accept, X-Hasura-Role headers) - `getPackageInfo` in `packages.go` implements exact name-match lookup using REST-first (`getPackageInfoREST`), GraphQL fallback (`getPackageInfoGraphQL`); `packageInfoCmd` in `main.go` resolves registries via `fetchRegistries` +- `jh scan` uses REST API endpoint `/api/v1/ui/vulnerabilities/packages/{name}` with optional `?version=` query param; no GraphQL fallback +- Scan concise output columns: ADVISORY, SEVERITY (top CVSS_V3 score or first available), AFFECTED (Yes/No/Unknown/- depending on whether `--version` was given), ALIASES, SUMMARY +- Scan `--verbose` flag shows full advisory details: advisory ID, affected status, summary, aliases, all severity scores, published/modified dates, affected versions, version ranges, details, and references +- Scan `--advisory ` flag filters to a single advisory (case-insensitive match on `advisory_id`) and prints a versions-focused view: summary, affected versions list, range events table; combine with `--verbose` for full details +- `printVersionsForAdvisory` in `scan.go` handles the `--advisory`-only output path +- `topSeverity` in `scan.go` prefers CVSS_V3 scores; falls back to first available score; returns "N/A" if none +- `affectedLabel` returns `-` when no version was queried, `Unknown` when `is_affected` is null, otherwise `Yes`/`No` ## Implementation Details diff --git a/README.md b/README.md index 390368b..8b14ea1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A command-line interface for interacting with JuliaHub, a platform for Julia com - **Git Integration**: Clone, push, fetch, and pull with automatic JuliaHub authentication - **Julia Integration**: Install Julia and run with JuliaHub package server configuration - **User Management**: Display user information and view profile details +- **Vulnerability Scanning**: Scan Julia packages for known security vulnerabilities - **Administrative Commands**: Manage users, tokens, and system resources (requires admin permissions) ## Installation @@ -218,6 +219,14 @@ go build -o jh . - `cat landing.md | jh admin landing-page update` - Read content from stdin - `jh admin landing-page remove` - Remove the custom landing page and revert to the default +### Vulnerability Scanning (`jh scan`) + +- `jh scan ` - Scan a Julia package for known security vulnerabilities + - Default: Shows a table with ADVISORY, SEVERITY, AFFECTED, ALIASES, and SUMMARY columns + - `--version ` - Check a specific version and show whether it is affected + - `--advisory ` - Show which versions of the package are affected by a specific advisory ID (e.g. a CVE or GHSA identifier) + - `--verbose` - Show full advisory details (aliases, severity scores, dates, version ranges, references, description); combinable with `--advisory` + ### Update (`jh update`) - `jh update` - Check for updates and automatically install the latest version @@ -368,6 +377,28 @@ cat landing.md | jh admin landing-page update jh admin landing-page remove ``` +### Vulnerability Scanning + +```bash +# Scan a package for known vulnerabilities +jh scan MbedTLS_jll + +# Scan a specific version (shows whether that version is affected) +jh scan MbedTLS_jll --version 2.28.1010+0 + +# Show which versions are affected by a specific advisory +jh scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz + +# Same, but with full advisory details +jh scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz --verbose + +# Show full advisory details for all vulnerabilities +jh scan MbedTLS_jll --version 2.28.1010+0 --verbose + +# Scan against a custom server +jh scan SomePackage -s nightly.juliahub.dev +``` + ### Git Workflow ```bash diff --git a/main.go b/main.go index 50d5dcb..6fe1c11 100644 --- a/main.go +++ b/main.go @@ -175,6 +175,7 @@ Available command categories: pull - Pull changes with authentication julia - Julia installation and management run - Run Julia with JuliaHub configuration + scan - Scan a package for known vulnerabilities Use 'jh --help' for more information about a specific command.`, } @@ -643,6 +644,59 @@ PROVIDER TYPES }` } +var scanCmd = &cobra.Command{ + Use: "scan ", + Short: "Scan a package for known vulnerabilities", + Long: `Show known security vulnerabilities for a Julia package. + +Displays vulnerability information including: +- Advisory ID (e.g. JLSEC-2024-001) +- Severity score (CVSS_V3 or other) +- CVE/GHSA aliases +- Summary description + +Optionally filter by a specific version to see whether that version is affected. +Use --advisory to show which versions are affected by a specific advisory. +Use --verbose for full details including version ranges, references, and descriptions.`, + Example: " jh scan MbedTLS_jll\n jh scan MbedTLS_jll --version 2.28.1010+0\n jh scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz\n jh scan MbedTLS_jll --version 2.28.1010+0 --verbose", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + server, err := getServerFromFlagOrConfig(cmd) + if err != nil { + fmt.Printf("Failed to get server config: %v\n", err) + os.Exit(1) + } + + packageName := args[0] + version, _ := cmd.Flags().GetString("version") + verbose, _ := cmd.Flags().GetBool("verbose") + advisory, _ := cmd.Flags().GetString("advisory") + + vulns, err := fetchVulnerabilities(server, packageName, version) + if err != nil { + fmt.Printf("Failed to fetch vulnerabilities: %v\n", err) + os.Exit(1) + } + + if advisory != "" { + for i, v := range vulns { + if strings.EqualFold(v.AdvisoryID, advisory) { + if verbose { + printVulnerabilities(packageName, version, []PackageVulnerability{vulns[i]}, true) + } else { + printVersionsForAdvisory(packageName, &vulns[i]) + } + return + } + } + fmt.Printf("Advisory %q not found for package %s.\n", advisory, packageName) + os.Exit(1) + } + + printVulnerabilities(packageName, version, vulns, verbose) + }, +} + var packageCmd = &cobra.Command{ Use: "package", Short: "Package search commands", @@ -1636,6 +1690,10 @@ func init() { fetchCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") pullCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") updateCmd.Flags().Bool("force", false, "Force update even if current version is newer than latest release") + scanCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") + scanCmd.Flags().StringP("version", "V", "", "Package version to check (shows whether that version is affected)") + scanCmd.Flags().BoolP("verbose", "v", false, "Show full vulnerability details") + scanCmd.Flags().StringP("advisory", "a", "", "Show which versions are affected by a specific advisory ID") authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd) jobCmd.AddCommand(jobListCmd, jobStartCmd) @@ -1658,7 +1716,7 @@ func init() { runCmd.AddCommand(runSetupCmd) gitCredentialCmd.AddCommand(gitCredentialHelperCmd, gitCredentialGetCmd, gitCredentialStoreCmd, gitCredentialEraseCmd, gitCredentialSetupCmd) - rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, packageCmd, registryCmd, userCmd, adminCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd) + rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, packageCmd, registryCmd, userCmd, adminCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd, scanCmd) } func main() { diff --git a/scan.go b/scan.go new file mode 100644 index 0000000..58cbc4c --- /dev/null +++ b/scan.go @@ -0,0 +1,244 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + "time" +) + +type SeverityScore struct { + Type string `json:"type"` + Score string `json:"score"` +} + +type RangeEvent struct { + EventType string `json:"event_type"` + Version string `json:"version"` +} + +type PackageVulnerability struct { + AdvisoryID string `json:"advisory_id"` + Summary string `json:"summary"` + Details string `json:"details"` + Published *time.Time `json:"published"` + Modified *time.Time `json:"modified"` + SeverityScores []SeverityScore `json:"severity_scores"` + Aliases []string `json:"aliases"` + References []string `json:"references"` + AffectedVersions []string `json:"affected_versions"` + RangesType string `json:"ranges_type"` + RangeEvents []RangeEvent `json:"range_events"` + IsAffected *bool `json:"is_affected"` +} + +func fetchVulnerabilities(server, packageName, version string) ([]PackageVulnerability, error) { + token, err := ensureValidToken() + if err != nil { + return nil, fmt.Errorf("authentication required: %w", err) + } + + endpoint := fmt.Sprintf("https://%s/api/v1/ui/vulnerabilities/packages/%s", + server, url.PathEscape(packageName)) + if version != "" { + endpoint += "?version=" + url.QueryEscape(version) + } + + body, err := apiGet(endpoint, token.IDToken) + if err != nil { + return nil, err + } + + var vulns []PackageVulnerability + if err := json.Unmarshal(body, &vulns); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + return vulns, nil +} + +// topSeverity returns the highest CVSS_V3 score string, or the first available, or "N/A". +func topSeverity(scores []SeverityScore) string { + for _, s := range scores { + if strings.HasPrefix(s.Type, "CVSS_V3") && s.Score != "" { + return s.Score + } + } + if len(scores) > 0 && scores[0].Score != "" { + return scores[0].Score + } + return "N/A" +} + +func affectedLabel(v *PackageVulnerability, versionQueried bool) string { + if !versionQueried { + return "-" + } + if v.IsAffected == nil { + return "Unknown" + } + if *v.IsAffected { + return "Yes" + } + return "No" +} + +func printVersionsForAdvisory(packageName string, v *PackageVulnerability) { + fmt.Printf("Package: %s\n", packageName) + fmt.Printf("Advisory: %s\n", v.AdvisoryID) + if len(v.Aliases) > 0 { + fmt.Printf("Aliases: %s\n", strings.Join(v.Aliases, ", ")) + } + if v.Summary != "" { + fmt.Printf("Summary: %s\n", v.Summary) + } + if v.Details != "" { + fmt.Printf("Details:\n %s\n", strings.ReplaceAll(strings.TrimSpace(v.Details), "\n", "\n ")) + } + fmt.Println() + + if len(v.AffectedVersions) > 0 { + fmt.Println("Affected versions:") + for _, av := range v.AffectedVersions { + fmt.Printf(" %s\n", av) + } + fmt.Println() + } + + if len(v.RangeEvents) > 0 { + rangeLabel := "Version ranges" + if v.RangesType != "" { + rangeLabel += fmt.Sprintf(" (%s)", v.RangesType) + } + fmt.Printf("%s:\n", rangeLabel) + for _, re := range v.RangeEvents { + fmt.Printf(" %-16s %s\n", re.EventType+":", re.Version) + } + fmt.Println() + } + + if len(v.AffectedVersions) == 0 && len(v.RangeEvents) == 0 { + fmt.Println("No version information available for this advisory.") + } +} + +func printVulnerabilities(packageName, version string, vulns []PackageVulnerability, verbose bool) { + versionQueried := version != "" + + header := fmt.Sprintf("Package: %s", packageName) + if versionQueried { + header += fmt.Sprintf(" (version: %s)", version) + } + fmt.Println(header) + fmt.Println() + + if len(vulns) == 0 { + fmt.Println("No vulnerabilities found.") + return + } + + count := len(vulns) + suffix := "ies" + if count == 1 { + suffix = "y" + } + fmt.Printf("Found %d vulnerabilit%s:\n\n", count, suffix) + + if verbose { + for i, v := range vulns { + if i > 0 { + fmt.Println(strings.Repeat("-", 60)) + } + fmt.Printf("Advisory: %s\n", v.AdvisoryID) + if versionQueried { + fmt.Printf("Affected: %s\n", affectedLabel(&v, versionQueried)) + } + if v.Summary != "" { + fmt.Printf("Summary: %s\n", v.Summary) + } + if len(v.Aliases) > 0 { + fmt.Printf("Aliases: %s\n", strings.Join(v.Aliases, ", ")) + } + if len(v.SeverityScores) > 0 { + parts := make([]string, len(v.SeverityScores)) + for i, s := range v.SeverityScores { + parts[i] = fmt.Sprintf("%s: %s", s.Type, s.Score) + } + fmt.Printf("Severity: %s\n", strings.Join(parts, ", ")) + } + if v.Published != nil { + fmt.Printf("Published: %s\n", v.Published.Local().Format("2006-01-02 15:04:05 MST")) + } + if v.Modified != nil { + fmt.Printf("Modified: %s\n", v.Modified.Local().Format("2006-01-02 15:04:05 MST")) + } + if len(v.AffectedVersions) > 0 { + fmt.Printf("Affected Versions: %s\n", strings.Join(v.AffectedVersions, ", ")) + } + if len(v.RangeEvents) > 0 { + rangeLabel := "Version Ranges" + if v.RangesType != "" { + rangeLabel += fmt.Sprintf(" (%s)", v.RangesType) + } + fmt.Printf("%s:\n", rangeLabel) + for _, re := range v.RangeEvents { + fmt.Printf(" %-16s %s\n", re.EventType+":", re.Version) + } + } + if v.Details != "" { + fmt.Printf("Details:\n %s\n", strings.ReplaceAll(strings.TrimSpace(v.Details), "\n", "\n ")) + } + if len(v.References) > 0 { + fmt.Printf("References:\n") + for _, ref := range v.References { + fmt.Printf(" - %s\n", ref) + } + } + fmt.Println() + } + return + } + + // Concise table + const ( + colAdvisory = 22 + colSeverity = 10 + colAffected = 10 + colAliases = 30 + colSummary = 50 + ) + + fmt.Printf("%-*s %-*s %-*s %-*s %s\n", + colAdvisory, "ADVISORY", + colSeverity, "SEVERITY", + colAffected, "AFFECTED", + colAliases, "ALIASES", + "SUMMARY") + fmt.Printf("%-*s %-*s %-*s %-*s %s\n", + colAdvisory, strings.Repeat("-", colAdvisory), + colSeverity, strings.Repeat("-", colSeverity), + colAffected, strings.Repeat("-", colAffected), + colAliases, strings.Repeat("-", colAliases), + strings.Repeat("-", colSummary)) + + for _, v := range vulns { + severity := topSeverity(v.SeverityScores) + + aliases := strings.Join(v.Aliases, ", ") + if len(aliases) > colAliases { + aliases = aliases[:colAliases-3] + "..." + } + + summary := v.Summary + if len(summary) > colSummary { + summary = summary[:colSummary-3] + "..." + } + + fmt.Printf("%-*s %-*s %-*s %-*s %s\n", + colAdvisory, v.AdvisoryID, + colSeverity, severity, + colAffected, affectedLabel(&v, versionQueried), + colAliases, aliases, + summary) + } +} From 23e2f50162067365a3cefdc1204b6a8d2103cdf9 Mon Sep 17 00:00:00 2001 From: Neeladri Das Date: Fri, 20 Mar 2026 12:45:29 +0000 Subject: [PATCH 2/4] added necessary flags and refactored the display format --- CLAUDE.md | 23 +++--- README.md | 31 +++++--- main.go | 81 +++++++++++++++------ scan.go | 214 ++++++++++++++++-------------------------------------- 4 files changed, 154 insertions(+), 195 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 56d248f..1fef949 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ The application follows a command-line interface pattern using the Cobra library - `jh admin user`: User management (list all users with REST API, supports verbose mode) - `jh admin token`: Token management (list all tokens with REST API, supports verbose mode) - `jh admin landing-page`: Landing page management (show/update/remove custom markdown landing page with REST API) - - `jh scan`: Vulnerability scanning for Julia packages (REST API; supports `--version` to check a specific version, `--advisory` to show which versions are affected by a specific advisory ID, and `--verbose` for full advisory details) + - `jh scan`: Vulnerability scanning for Julia packages (REST API; defaults to latest stable version via `GET /docs///versions.json`; supports `--version` for a specific version, `--registry` to specify the registry for version lookup (default: General), `--advisory` to filter to a specific advisory ID, `--all` to show all advisories regardless of affected status, and `--verbose` for additional details) - `jh clone`: Git clone with JuliaHub authentication and project name resolution - `jh push/fetch/pull`: Git operations with JuliaHub authentication - `jh git-credential`: Git credential helper for seamless authentication @@ -164,9 +164,11 @@ go run . admin landing-page remove ```bash go run . scan MbedTLS_jll go run . scan MbedTLS_jll --version 2.28.1010+0 -go run . scan MbedTLS_jll --version 2.28.1010+0 --verbose -go run . scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz -go run . scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz --verbose +go run . scan MbedTLS_jll --all +go run . scan MbedTLS_jll --advisory JLSEC-2025-232 +go run . scan MbedTLS_jll --advisory JLSEC-2025-232 --verbose +go run . scan MbedTLS_jll --verbose +go run . scan MyPkg --registry MyRegistry go run . scan SomePackage -s nightly.juliahub.dev ``` @@ -382,13 +384,14 @@ jh run setup - GraphQL fallback uses `package_search_with_count.gql` which fetches both the package list and aggregate count in a single request (`package_search` + `package_search_aggregate` root fields) - `executeGraphQL(server, token, req)` in `packages.go` is a shared helper for GraphQL POST requests (sets Authorization, Content-Type, Accept, X-Hasura-Role headers) - `getPackageInfo` in `packages.go` implements exact name-match lookup using REST-first (`getPackageInfoREST`), GraphQL fallback (`getPackageInfoGraphQL`); `packageInfoCmd` in `main.go` resolves registries via `fetchRegistries` -- `jh scan` uses REST API endpoint `/api/v1/ui/vulnerabilities/packages/{name}` with optional `?version=` query param; no GraphQL fallback -- Scan concise output columns: ADVISORY, SEVERITY (top CVSS_V3 score or first available), AFFECTED (Yes/No/Unknown/- depending on whether `--version` was given), ALIASES, SUMMARY -- Scan `--verbose` flag shows full advisory details: advisory ID, affected status, summary, aliases, all severity scores, published/modified dates, affected versions, version ranges, details, and references -- Scan `--advisory ` flag filters to a single advisory (case-insensitive match on `advisory_id`) and prints a versions-focused view: summary, affected versions list, range events table; combine with `--verbose` for full details -- `printVersionsForAdvisory` in `scan.go` handles the `--advisory`-only output path +- `jh scan` uses two REST endpoints: vulnerabilities at `/api/v1/ui/vulnerabilities/packages/{name}?version=` and latest version at `/docs///versions.json` (first entry is latest); no GraphQL fallback +- When no `--version` is given, `fetchLatestVersion` calls the versions.json endpoint (registry defaults to `General`, overridable with `--registry`) +- By default only advisories where `is_affected == true` are shown; `--all` overrides this +- Each advisory is printed as: Advisory ID (clickable OSC8 hyperlink to JuliaLang SecurityAdvisories), Affected (Yes/No), Severity scores (all, comma-separated), full Summary, Affected versions (one line), Version ranges (one line, with ranges type) +- `--advisory ` filters to a single advisory (case-insensitive match); same output format +- `--verbose` adds: Aliases, Published date, Modified date, References +- `advisoryLink` in `scan.go` builds the OSC8 terminal hyperlink to `https://github.com/JuliaLang/SecurityAdvisories.jl/blob/main/advisories/published//.md` - `topSeverity` in `scan.go` prefers CVSS_V3 scores; falls back to first available score; returns "N/A" if none -- `affectedLabel` returns `-` when no version was queried, `Unknown` when `is_affected` is null, otherwise `Yes`/`No` ## Implementation Details diff --git a/README.md b/README.md index 8b14ea1..4f6ab87 100644 --- a/README.md +++ b/README.md @@ -222,10 +222,14 @@ go build -o jh . ### Vulnerability Scanning (`jh scan`) - `jh scan ` - Scan a Julia package for known security vulnerabilities - - Default: Shows a table with ADVISORY, SEVERITY, AFFECTED, ALIASES, and SUMMARY columns - - `--version ` - Check a specific version and show whether it is affected - - `--advisory ` - Show which versions of the package are affected by a specific advisory ID (e.g. a CVE or GHSA identifier) - - `--verbose` - Show full advisory details (aliases, severity scores, dates, version ranges, references, description); combinable with `--advisory` + - Defaults to the latest stable version (fetched from the registry); only shows advisories where that version is affected + - `--version ` - Check a specific version instead of the latest + - `--registry ` - Registry to use for version lookup (default: `General`) + - `--advisory ` - Filter to a specific advisory ID (e.g. a CVE or GHSA identifier) + - `--all` - Show all advisories regardless of whether the queried version is affected + - `--verbose` / `-v` - Show additional details: aliases, published/modified dates, and references + - Advisory IDs are clickable links to the JuliaLang SecurityAdvisories repository + - Each advisory shows: severity scores, affected status, full summary, affected versions, and version ranges ### Update (`jh update`) @@ -380,20 +384,23 @@ jh admin landing-page remove ### Vulnerability Scanning ```bash -# Scan a package for known vulnerabilities +# Scan latest stable version (only shows advisories where it is affected) jh scan MbedTLS_jll -# Scan a specific version (shows whether that version is affected) +# Scan a specific version jh scan MbedTLS_jll --version 2.28.1010+0 -# Show which versions are affected by a specific advisory -jh scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz +# Show all advisories regardless of affected status +jh scan MbedTLS_jll --all -# Same, but with full advisory details -jh scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz --verbose +# Filter to a specific advisory +jh scan MbedTLS_jll --advisory JLSEC-2025-232 -# Show full advisory details for all vulnerabilities -jh scan MbedTLS_jll --version 2.28.1010+0 --verbose +# Show extra details (aliases, dates, references) +jh scan MbedTLS_jll --verbose + +# Use a non-General registry for version lookup +jh scan MyPkg --registry MyRegistry # Scan against a custom server jh scan SomePackage -s nightly.juliahub.dev diff --git a/main.go b/main.go index 6fe1c11..9180a76 100644 --- a/main.go +++ b/main.go @@ -649,16 +649,12 @@ var scanCmd = &cobra.Command{ Short: "Scan a package for known vulnerabilities", Long: `Show known security vulnerabilities for a Julia package. -Displays vulnerability information including: -- Advisory ID (e.g. JLSEC-2024-001) -- Severity score (CVSS_V3 or other) -- CVE/GHSA aliases -- Summary description - -Optionally filter by a specific version to see whether that version is affected. -Use --advisory to show which versions are affected by a specific advisory. -Use --verbose for full details including version ranges, references, and descriptions.`, - Example: " jh scan MbedTLS_jll\n jh scan MbedTLS_jll --version 2.28.1010+0\n jh scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz\n jh scan MbedTLS_jll --version 2.28.1010+0 --verbose", +Defaults to checking the latest stable version of the package. Use --version to +check a specific version. Only advisories that affect the queried version are shown +by default; use --all to list all advisories regardless of affected status. + +Use --advisory to look up a specific advisory by ID.`, + Example: " jh scan MbedTLS_jll\n jh scan MbedTLS_jll --version 2.28.1010+0\n jh scan MbedTLS_jll --all\n jh scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { server, err := getServerFromFlagOrConfig(cmd) @@ -669,8 +665,19 @@ Use --verbose for full details including version ranges, references, and descrip packageName := args[0] version, _ := cmd.Flags().GetString("version") - verbose, _ := cmd.Flags().GetBool("verbose") advisory, _ := cmd.Flags().GetString("advisory") + registry, _ := cmd.Flags().GetString("registry") + all, _ := cmd.Flags().GetBool("all") + verbose, _ := cmd.Flags().GetBool("verbose") + + if version == "" { + latest, err := fetchLatestVersion(server, registry, packageName) + if err != nil { + fmt.Printf("Failed to fetch latest version: %v\n", err) + os.Exit(1) + } + version = latest + } vulns, err := fetchVulnerabilities(server, packageName, version) if err != nil { @@ -678,14 +685,17 @@ Use --verbose for full details including version ranges, references, and descrip os.Exit(1) } + fmt.Printf("Package: %s", packageName) + if version != "" { + fmt.Printf(" (%s)", version) + } + fmt.Println() + fmt.Println() + if advisory != "" { - for i, v := range vulns { + for _, v := range vulns { if strings.EqualFold(v.AdvisoryID, advisory) { - if verbose { - printVulnerabilities(packageName, version, []PackageVulnerability{vulns[i]}, true) - } else { - printVersionsForAdvisory(packageName, &vulns[i]) - } + printAdvisory(&v, true, verbose) return } } @@ -693,7 +703,34 @@ Use --verbose for full details including version ranges, references, and descrip os.Exit(1) } - printVulnerabilities(packageName, version, vulns, verbose) + var toShow []PackageVulnerability + for _, v := range vulns { + if all || (v.IsAffected != nil && *v.IsAffected) { + toShow = append(toShow, v) + } + } + + if len(toShow) == 0 { + if all { + fmt.Println("No vulnerabilities found.") + } else { + fmt.Println("No known vulnerabilities affecting this version.") + } + return + } + + suffix := "ies" + if len(toShow) == 1 { + suffix = "y" + } + fmt.Printf("Found %d vulnerabilit%s:\n\n", len(toShow), suffix) + + for i := range toShow { + if i > 0 { + fmt.Println() + } + printAdvisory(&toShow[i], false, verbose) + } }, } @@ -1691,9 +1728,11 @@ func init() { pullCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") updateCmd.Flags().Bool("force", false, "Force update even if current version is newer than latest release") scanCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") - scanCmd.Flags().StringP("version", "V", "", "Package version to check (shows whether that version is affected)") - scanCmd.Flags().BoolP("verbose", "v", false, "Show full vulnerability details") - scanCmd.Flags().StringP("advisory", "a", "", "Show which versions are affected by a specific advisory ID") + scanCmd.Flags().StringP("version", "V", "", "Package version to check (defaults to latest stable)") + scanCmd.Flags().StringP("advisory", "a", "", "Look up a specific advisory by ID") + scanCmd.Flags().StringP("registry", "r", "General", "Registry name for version lookup") + scanCmd.Flags().Bool("all", false, "Show all advisories regardless of affected status") + scanCmd.Flags().BoolP("verbose", "v", false, "Show full advisory details (aliases, dates, details, references)") authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd) jobCmd.AddCommand(jobListCmd, jobStartCmd) diff --git a/scan.go b/scan.go index 58cbc4c..f289645 100644 --- a/scan.go +++ b/scan.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "net/url" - "strings" +"strings" "time" ) @@ -57,6 +57,28 @@ func fetchVulnerabilities(server, packageName, version string) ([]PackageVulnera return vulns, nil } +func fetchLatestVersion(server, registry, packageName string) (string, error) { + token, err := ensureValidToken() + if err != nil { + return "", fmt.Errorf("authentication required: %w", err) + } + + endpoint := fmt.Sprintf("https://%s/docs/%s/%s/versions.json", server, url.PathEscape(registry), url.PathEscape(packageName)) + body, err := apiGet(endpoint, token.IDToken) + if err != nil { + return "", err + } + + var versions []string + if err := json.Unmarshal(body, &versions); err != nil { + return "", fmt.Errorf("failed to parse versions: %w", err) + } + if len(versions) == 0 { + return "", fmt.Errorf("no versions found for %s", packageName) + } + return versions[0], nil +} + // topSeverity returns the highest CVSS_V3 score string, or the first available, or "N/A". func topSeverity(scores []SeverityScore) string { for _, s := range scores { @@ -70,175 +92,63 @@ func topSeverity(scores []SeverityScore) string { return "N/A" } -func affectedLabel(v *PackageVulnerability, versionQueried bool) string { - if !versionQueried { - return "-" - } - if v.IsAffected == nil { - return "Unknown" - } - if *v.IsAffected { - return "Yes" +func advisoryLink(v *PackageVulnerability) string { + year := "unknown" + if v.Published != nil { + year = fmt.Sprintf("%d", v.Published.Year()) } - return "No" + url := fmt.Sprintf("https://github.com/JuliaLang/SecurityAdvisories.jl/blob/main/advisories/published/%s/%s.md", year, v.AdvisoryID) + return fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", url, v.AdvisoryID) } -func printVersionsForAdvisory(packageName string, v *PackageVulnerability) { - fmt.Printf("Package: %s\n", packageName) - fmt.Printf("Advisory: %s\n", v.AdvisoryID) - if len(v.Aliases) > 0 { - fmt.Printf("Aliases: %s\n", strings.Join(v.Aliases, ", ")) +func printAdvisory(v *PackageVulnerability, showRanges bool, verbose bool) { + fmt.Printf("Advisory: %s\n", advisoryLink(v)) + if v.IsAffected != nil { + if *v.IsAffected { + fmt.Println("Affected: Yes") + } else { + fmt.Println("Affected: No") + } + } + if len(v.SeverityScores) > 0 { + parts := make([]string, len(v.SeverityScores)) + for i, s := range v.SeverityScores { + parts[i] = s.Type + ": " + s.Score + } + fmt.Printf("Severity: %s\n", strings.Join(parts, ", ")) } if v.Summary != "" { fmt.Printf("Summary: %s\n", v.Summary) } - if v.Details != "" { - fmt.Printf("Details:\n %s\n", strings.ReplaceAll(strings.TrimSpace(v.Details), "\n", "\n ")) - } - fmt.Println() - if len(v.AffectedVersions) > 0 { - fmt.Println("Affected versions:") - for _, av := range v.AffectedVersions { - fmt.Printf(" %s\n", av) - } - fmt.Println() + fmt.Printf("Affected versions: %s\n", strings.Join(v.AffectedVersions, ", ")) } - if len(v.RangeEvents) > 0 { + parts := make([]string, len(v.RangeEvents)) + for i, re := range v.RangeEvents { + parts[i] = re.EventType + ": " + re.Version + } rangeLabel := "Version ranges" if v.RangesType != "" { rangeLabel += fmt.Sprintf(" (%s)", v.RangesType) } - fmt.Printf("%s:\n", rangeLabel) - for _, re := range v.RangeEvents { - fmt.Printf(" %-16s %s\n", re.EventType+":", re.Version) - } - fmt.Println() - } - - if len(v.AffectedVersions) == 0 && len(v.RangeEvents) == 0 { - fmt.Println("No version information available for this advisory.") - } -} - -func printVulnerabilities(packageName, version string, vulns []PackageVulnerability, verbose bool) { - versionQueried := version != "" - - header := fmt.Sprintf("Package: %s", packageName) - if versionQueried { - header += fmt.Sprintf(" (version: %s)", version) - } - fmt.Println(header) - fmt.Println() - - if len(vulns) == 0 { - fmt.Println("No vulnerabilities found.") - return + fmt.Printf("%s: %s\n", rangeLabel, strings.Join(parts, ", ")) } - - count := len(vulns) - suffix := "ies" - if count == 1 { - suffix = "y" - } - fmt.Printf("Found %d vulnerabilit%s:\n\n", count, suffix) - if verbose { - for i, v := range vulns { - if i > 0 { - fmt.Println(strings.Repeat("-", 60)) - } - fmt.Printf("Advisory: %s\n", v.AdvisoryID) - if versionQueried { - fmt.Printf("Affected: %s\n", affectedLabel(&v, versionQueried)) - } - if v.Summary != "" { - fmt.Printf("Summary: %s\n", v.Summary) - } - if len(v.Aliases) > 0 { - fmt.Printf("Aliases: %s\n", strings.Join(v.Aliases, ", ")) - } - if len(v.SeverityScores) > 0 { - parts := make([]string, len(v.SeverityScores)) - for i, s := range v.SeverityScores { - parts[i] = fmt.Sprintf("%s: %s", s.Type, s.Score) - } - fmt.Printf("Severity: %s\n", strings.Join(parts, ", ")) - } - if v.Published != nil { - fmt.Printf("Published: %s\n", v.Published.Local().Format("2006-01-02 15:04:05 MST")) - } - if v.Modified != nil { - fmt.Printf("Modified: %s\n", v.Modified.Local().Format("2006-01-02 15:04:05 MST")) - } - if len(v.AffectedVersions) > 0 { - fmt.Printf("Affected Versions: %s\n", strings.Join(v.AffectedVersions, ", ")) - } - if len(v.RangeEvents) > 0 { - rangeLabel := "Version Ranges" - if v.RangesType != "" { - rangeLabel += fmt.Sprintf(" (%s)", v.RangesType) - } - fmt.Printf("%s:\n", rangeLabel) - for _, re := range v.RangeEvents { - fmt.Printf(" %-16s %s\n", re.EventType+":", re.Version) - } - } - if v.Details != "" { - fmt.Printf("Details:\n %s\n", strings.ReplaceAll(strings.TrimSpace(v.Details), "\n", "\n ")) - } - if len(v.References) > 0 { - fmt.Printf("References:\n") - for _, ref := range v.References { - fmt.Printf(" - %s\n", ref) - } - } - fmt.Println() + if len(v.Aliases) > 0 { + fmt.Printf("Aliases: %s\n", strings.Join(v.Aliases, ", ")) } - return - } - - // Concise table - const ( - colAdvisory = 22 - colSeverity = 10 - colAffected = 10 - colAliases = 30 - colSummary = 50 - ) - - fmt.Printf("%-*s %-*s %-*s %-*s %s\n", - colAdvisory, "ADVISORY", - colSeverity, "SEVERITY", - colAffected, "AFFECTED", - colAliases, "ALIASES", - "SUMMARY") - fmt.Printf("%-*s %-*s %-*s %-*s %s\n", - colAdvisory, strings.Repeat("-", colAdvisory), - colSeverity, strings.Repeat("-", colSeverity), - colAffected, strings.Repeat("-", colAffected), - colAliases, strings.Repeat("-", colAliases), - strings.Repeat("-", colSummary)) - - for _, v := range vulns { - severity := topSeverity(v.SeverityScores) - - aliases := strings.Join(v.Aliases, ", ") - if len(aliases) > colAliases { - aliases = aliases[:colAliases-3] + "..." + if v.Published != nil { + fmt.Printf("Published: %s\n", v.Published.Format("2006-01-02")) } - - summary := v.Summary - if len(summary) > colSummary { - summary = summary[:colSummary-3] + "..." + if v.Modified != nil { + fmt.Printf("Modified: %s\n", v.Modified.Format("2006-01-02")) + } +if len(v.References) > 0 { + fmt.Println("References:") + for _, r := range v.References { + fmt.Printf(" %s\n", r) + } } - - fmt.Printf("%-*s %-*s %-*s %-*s %s\n", - colAdvisory, v.AdvisoryID, - colSeverity, severity, - colAffected, affectedLabel(&v, versionQueried), - colAliases, aliases, - summary) } } From 0bc1bf7d6aa2982db9b5528afb2cbf5bdef03804 Mon Sep 17 00:00:00 2001 From: Neeladri Das Date: Fri, 20 Mar 2026 13:14:16 +0000 Subject: [PATCH 3/4] changed the command to vuln --- CLAUDE.md | 26 +++++++++++++------------- README.md | 18 +++++++++--------- main.go | 20 ++++++++++---------- scan.go => vuln.go | 4 ++-- 4 files changed, 34 insertions(+), 34 deletions(-) rename scan.go => vuln.go (99%) diff --git a/CLAUDE.md b/CLAUDE.md index 6ec28e2..459774a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ The application follows a command-line interface pattern using the Cobra library - **user.go**: User information retrieval using GraphQL API and REST API for listing users - **tokens.go**: Token management operations (list) with REST API integration - **landing.go**: Landing page management (show, update, remove) with REST API integration -- **scan.go**: Vulnerability scanning for Julia packages via REST API +- **vuln.go**: Vulnerability scanning for Julia packages via REST API - **git.go**: Git integration (clone, push, fetch, pull) with JuliaHub authentication - **julia.go**: Julia installation and management - **run.go**: Julia execution with JuliaHub configuration @@ -50,7 +50,7 @@ The application follows a command-line interface pattern using the Cobra library - `jh admin user`: User management (list all users with REST API, supports verbose mode) - `jh admin token`: Token management (list all tokens with REST API, supports verbose mode) - `jh admin landing-page`: Landing page management (show/update/remove custom markdown landing page with REST API) - - `jh scan`: Vulnerability scanning for Julia packages (REST API; defaults to latest stable version via `GET /docs///versions.json`; supports `--version` for a specific version, `--registry` to specify the registry for version lookup (default: General), `--advisory` to filter to a specific advisory ID, `--all` to show all advisories regardless of affected status, and `--verbose` for additional details) + - `jh vuln`: Vulnerability scanning for Julia packages (REST API; defaults to latest stable version via `GET /docs///versions.json`; supports `--version` for a specific version, `--registry` to specify the registry for version lookup (default: General), `--advisory` to filter to a specific advisory ID, `--all` to show all advisories regardless of affected status, and `--verbose` for additional details) - `jh clone`: Git clone with JuliaHub authentication and project name resolution - `jh push/fetch/pull`: Git operations with JuliaHub authentication - `jh git-credential`: Git credential helper for seamless authentication @@ -166,14 +166,14 @@ go run . admin landing-page remove ### Test vulnerability scan operations ```bash -go run . scan MbedTLS_jll -go run . scan MbedTLS_jll --version 2.28.1010+0 -go run . scan MbedTLS_jll --all -go run . scan MbedTLS_jll --advisory JLSEC-2025-232 -go run . scan MbedTLS_jll --advisory JLSEC-2025-232 --verbose -go run . scan MbedTLS_jll --verbose -go run . scan MyPkg --registry MyRegistry -go run . scan SomePackage -s nightly.juliahub.dev +go run . vuln MbedTLS_jll +go run . vuln MbedTLS_jll --version 2.28.1010+0 +go run . vuln MbedTLS_jll --all +go run . vuln MbedTLS_jll --advisory JLSEC-2025-232 +go run . vuln MbedTLS_jll --advisory JLSEC-2025-232 --verbose +go run . vuln MbedTLS_jll --verbose +go run . vuln MyPkg --registry MyRegistry +go run . vuln SomePackage -s nightly.juliahub.dev ``` ### Test Git operations @@ -425,14 +425,14 @@ jh run setup - `executeGraphQL(server, token, req)` in `packages.go` is a shared helper for GraphQL POST requests (sets Authorization, Content-Type, Accept, X-Hasura-Role headers) - `getPackageInfo` in `packages.go` implements exact name-match lookup using REST-first (`getPackageInfoREST`), GraphQL fallback (`getPackageInfoGraphQL`); `packageInfoCmd` in `main.go` resolves registries via `fetchRegistries` - `getPackageDependencies` uses GraphQL (`fetchGraphQLPackages`) to locate the package, then fetches `/docs/{registry}/{package}/stable/pkg.json` for dependency data; no REST fallback (docs endpoint is authoritative) -- `jh scan` uses two REST endpoints: vulnerabilities at `/api/v1/ui/vulnerabilities/packages/{name}?version=` and latest version at `/docs///versions.json` (first entry is latest); no GraphQL fallback +- `jh vuln` uses two REST endpoints: vulnerabilities at `/api/v1/ui/vulnerabilities/packages/{name}?version=` and latest version at `/docs///versions.json` (first entry is latest); no GraphQL fallback - When no `--version` is given, `fetchLatestVersion` calls the versions.json endpoint (registry defaults to `General`, overridable with `--registry`) - By default only advisories where `is_affected == true` are shown; `--all` overrides this - Each advisory is printed as: Advisory ID (clickable OSC8 hyperlink to JuliaLang SecurityAdvisories), Affected (Yes/No), Severity scores (all, comma-separated), full Summary, Affected versions (one line), Version ranges (one line, with ranges type) - `--advisory ` filters to a single advisory (case-insensitive match); same output format - `--verbose` adds: Aliases, Published date, Modified date, References -- `advisoryLink` in `scan.go` builds the OSC8 terminal hyperlink to `https://github.com/JuliaLang/SecurityAdvisories.jl/blob/main/advisories/published//.md` -- `topSeverity` in `scan.go` prefers CVSS_V3 scores; falls back to first available score; returns "N/A" if none +- `advisoryLink` in `vuln.go` builds the OSC8 terminal hyperlink to `https://github.com/JuliaLang/SecurityAdvisories.jl/blob/main/advisories/published//.md` +- `topSeverity` in `vuln.go` prefers CVSS_V3 scores; falls back to first available score; returns "N/A" if none ## Implementation Details diff --git a/README.md b/README.md index b890013..d030751 100644 --- a/README.md +++ b/README.md @@ -224,9 +224,9 @@ go build -o jh . - `cat landing.md | jh admin landing-page update` - Read content from stdin - `jh admin landing-page remove` - Remove the custom landing page and revert to the default -### Vulnerability Scanning (`jh scan`) +### Vulnerability Scanning (`jh vuln`) -- `jh scan ` - Scan a Julia package for known security vulnerabilities +- `jh vuln ` - Show known vulnerabilities for a Julia package - Defaults to the latest stable version (fetched from the registry); only shows advisories where that version is affected - `--version ` - Check a specific version instead of the latest - `--registry ` - Registry to use for version lookup (default: `General`) @@ -395,25 +395,25 @@ jh admin landing-page remove ```bash # Scan latest stable version (only shows advisories where it is affected) -jh scan MbedTLS_jll +jh vuln MbedTLS_jll # Scan a specific version -jh scan MbedTLS_jll --version 2.28.1010+0 +jh vuln MbedTLS_jll --version 2.28.1010+0 # Show all advisories regardless of affected status -jh scan MbedTLS_jll --all +jh vuln MbedTLS_jll --all # Filter to a specific advisory -jh scan MbedTLS_jll --advisory JLSEC-2025-232 +jh vuln MbedTLS_jll --advisory JLSEC-2025-232 # Show extra details (aliases, dates, references) -jh scan MbedTLS_jll --verbose +jh vuln MbedTLS_jll --verbose # Use a non-General registry for version lookup -jh scan MyPkg --registry MyRegistry +jh vuln MyPkg --registry MyRegistry # Scan against a custom server -jh scan SomePackage -s nightly.juliahub.dev +jh vuln SomePackage -s nightly.juliahub.dev ``` ### Git Workflow diff --git a/main.go b/main.go index 48c5dcd..91e20f0 100644 --- a/main.go +++ b/main.go @@ -644,9 +644,9 @@ PROVIDER TYPES }` } -var scanCmd = &cobra.Command{ - Use: "scan ", - Short: "Scan a package for known vulnerabilities", +var vulnCmd = &cobra.Command{ + Use: "vuln ", + Short: "Show known vulnerabilities for a package", Long: `Show known security vulnerabilities for a Julia package. Defaults to checking the latest stable version of the package. Use --version to @@ -1762,12 +1762,12 @@ func init() { fetchCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") pullCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") updateCmd.Flags().Bool("force", false, "Force update even if current version is newer than latest release") - scanCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") - scanCmd.Flags().StringP("version", "V", "", "Package version to check (defaults to latest stable)") - scanCmd.Flags().StringP("advisory", "a", "", "Look up a specific advisory by ID") - scanCmd.Flags().StringP("registry", "r", "General", "Registry name for version lookup") - scanCmd.Flags().Bool("all", false, "Show all advisories regardless of affected status") - scanCmd.Flags().BoolP("verbose", "v", false, "Show full advisory details (aliases, dates, details, references)") + vulnCmd.Flags().StringP("server", "s", "juliahub.com", "JuliaHub server") + vulnCmd.Flags().StringP("version", "V", "", "Package version to check (defaults to latest stable)") + vulnCmd.Flags().StringP("advisory", "a", "", "Look up a specific advisory by ID") + vulnCmd.Flags().StringP("registry", "r", "General", "Registry name for version lookup") + vulnCmd.Flags().Bool("all", false, "Show all advisories regardless of affected status") + vulnCmd.Flags().BoolP("verbose", "v", false, "Show full advisory details (aliases, dates, details, references)") authCmd.AddCommand(authLoginCmd, authRefreshCmd, authStatusCmd, authEnvCmd) jobCmd.AddCommand(jobListCmd, jobStartCmd) @@ -1790,7 +1790,7 @@ func init() { runCmd.AddCommand(runSetupCmd) gitCredentialCmd.AddCommand(gitCredentialHelperCmd, gitCredentialGetCmd, gitCredentialStoreCmd, gitCredentialEraseCmd, gitCredentialSetupCmd) - rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, packageCmd, registryCmd, userCmd, adminCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd, scanCmd) + rootCmd.AddCommand(authCmd, jobCmd, datasetCmd, projectCmd, packageCmd, registryCmd, userCmd, adminCmd, juliaCmd, cloneCmd, pushCmd, fetchCmd, pullCmd, runCmd, gitCredentialCmd, updateCmd, vulnCmd) } func main() { diff --git a/scan.go b/vuln.go similarity index 99% rename from scan.go rename to vuln.go index f289645..04ac4bb 100644 --- a/scan.go +++ b/vuln.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "net/url" -"strings" + "strings" "time" ) @@ -144,7 +144,7 @@ func printAdvisory(v *PackageVulnerability, showRanges bool, verbose bool) { if v.Modified != nil { fmt.Printf("Modified: %s\n", v.Modified.Format("2006-01-02")) } -if len(v.References) > 0 { + if len(v.References) > 0 { fmt.Println("References:") for _, r := range v.References { fmt.Printf(" %s\n", r) From e7037d3f49f4ae5edb435303c4d8e2705377febd Mon Sep 17 00:00:00 2001 From: Neeladri Das Date: Tue, 14 Apr 2026 07:27:04 +0000 Subject: [PATCH 4/4] removed redundancies --- main.go | 8 ++++---- vuln.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index ccdb660..6aaa32a 100644 --- a/main.go +++ b/main.go @@ -175,7 +175,7 @@ Available command categories: pull - Pull changes with authentication julia - Julia installation and management run - Run Julia with JuliaHub configuration - scan - Scan a package for known vulnerabilities + vuln - Scan a package for known vulnerabilities Use 'jh --help' for more information about a specific command.`, } @@ -654,7 +654,7 @@ check a specific version. Only advisories that affect the queried version are sh by default; use --all to list all advisories regardless of affected status. Use --advisory to look up a specific advisory by ID.`, - Example: " jh scan MbedTLS_jll\n jh scan MbedTLS_jll --version 2.28.1010+0\n jh scan MbedTLS_jll --all\n jh scan MbedTLS_jll --advisory GHSA-xxx-yyy-zzz", + Example: " jh vuln MbedTLS_jll\n jh vuln MbedTLS_jll --version 2.28.1010+0\n jh vuln MbedTLS_jll --all\n jh vuln MbedTLS_jll --advisory JLSEC-2025-232", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { server, err := getServerFromFlagOrConfig(cmd) @@ -695,7 +695,7 @@ Use --advisory to look up a specific advisory by ID.`, if advisory != "" { for _, v := range vulns { if strings.EqualFold(v.AdvisoryID, advisory) { - printAdvisory(&v, true, verbose) + printAdvisory(&v, verbose) return } } @@ -729,7 +729,7 @@ Use --advisory to look up a specific advisory by ID.`, if i > 0 { fmt.Println() } - printAdvisory(&toShow[i], false, verbose) + printAdvisory(&toShow[i], verbose) } }, } diff --git a/vuln.go b/vuln.go index 04ac4bb..f40e66e 100644 --- a/vuln.go +++ b/vuln.go @@ -101,7 +101,7 @@ func advisoryLink(v *PackageVulnerability) string { return fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", url, v.AdvisoryID) } -func printAdvisory(v *PackageVulnerability, showRanges bool, verbose bool) { +func printAdvisory(v *PackageVulnerability, verbose bool) { fmt.Printf("Advisory: %s\n", advisoryLink(v)) if v.IsAffected != nil { if *v.IsAffected {