From 9fd3ff7957580388920fdb4f085b4d7946bb0740 Mon Sep 17 00:00:00 2001 From: Zachary LaVallee Date: Tue, 28 Jul 2026 09:48:59 -0700 Subject: [PATCH 1/2] feat(sbom): add --json output to fossa sbom analyze Add a `--json` flag to `fossa sbom analyze` that, on a successful upload, prints project metadata as JSON to stdout using the same renderer and schema as `fossa analyze --json`, so downstream tooling parses both identically. The emitted locator carries the SBOM project locator (`sbom+/`) and full id needed for follow-up `fossa project edit` calls. Reuse analyze's `buildProjectSummary` (now exported) and the existing `JsonOutput` flag helper; the human "View FOSSA Report" banner is kept alongside the JSON, matching analyze's behavior. Co-Authored-By: Claude Opus 4.8 --- Changelog.md | 4 ++++ docs/references/subcommands/sbom.md | 13 +++++++++++++ src/App/Fossa/Analyze/Upload.hs | 1 + src/App/Fossa/Config/SBOM/Analyze.hs | 4 ++++ src/App/Fossa/SBOM/Analyze.hs | 16 ++++++++++++++-- test/App/Fossa/SBOMAnalyzeSpec.hs | 17 ++++++++++++++--- 6 files changed, 50 insertions(+), 5 deletions(-) diff --git a/Changelog.md b/Changelog.md index 4ccb1bdb9a..1b14eb5997 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # FOSSA CLI Changelog +## Unreleased + +- SBOM: `fossa sbom analyze --json` prints project metadata as JSON, matching `fossa analyze --json`. ([#XXXX](https://github.com/fossas/fossa-cli/pull/XXXX)) + ## 3.17.15 - Node: Workspaces declared with a leading `./` (for example `./packages/*`) are now matched, so their members are analyzed and their production dependencies are no longer dropped from the results. ([#1733](https://github.com/fossas/fossa-cli/pull/1733)) diff --git a/docs/references/subcommands/sbom.md b/docs/references/subcommands/sbom.md index 934b54195c..27450c45ea 100644 --- a/docs/references/subcommands/sbom.md +++ b/docs/references/subcommands/sbom.md @@ -25,6 +25,19 @@ In addition to the [usual FOSSA project flags](#common-fossa-project-flags) supp | ------------------------------------- | ----- | ----------------------------------------------------------------------------------- | | `--team 'team name'` | `-T` | Specify a team within your FOSSA organization. If you only have team-scoped permissions, you must specify a team of which you are a member. | | `--force-rescan` | | Force the SBOM file to be rescanned, even if this exact revision has been previously uploaded | +| `--json` | | Output project metadata as JSON to the console after a successful upload. This is useful for communicating with the FOSSA API. | + +### Printing project metadata + +The `--json` flag prints project metadata to stdout after `fossa sbom analyze` uploads successfully, in the same schema as [`fossa analyze --json`](./analyze.md#printing-project-metadata). This metadata can be used to reference your project when integrating with the FOSSA API. SBOM projects use the `sbom` fetcher (not `custom`), so the locator is `sbom+/`. + +```sh +fossa sbom analyze /path/to/sampleCycloneDX.json --json +``` + +```json +{"branch":null, "id":"sbom+/sampleCycloneDX$123", "project":"/sampleCycloneDX", "projectId":"sbom+/sampleCycloneDX", "revision":"123", "url":"https://app.fossa.com/projects/sbom%2b%2fsampleCycloneDX/refs/branch/master/123"} +``` ### Team Permissions diff --git a/src/App/Fossa/Analyze/Upload.hs b/src/App/Fossa/Analyze/Upload.hs index 9ee714428b..55074a3e90 100644 --- a/src/App/Fossa/Analyze/Upload.hs +++ b/src/App/Fossa/Analyze/Upload.hs @@ -3,6 +3,7 @@ module App.Fossa.Analyze.Upload ( mergeSourceAndLicenseUnits, uploadSuccessfulAnalysis, + buildProjectSummary, emitBuildWarnings, ScanUnits (..), ) where diff --git a/src/App/Fossa/Config/SBOM/Analyze.hs b/src/App/Fossa/Config/SBOM/Analyze.hs index 03c45c9331..73a75ae757 100644 --- a/src/App/Fossa/Config/SBOM/Analyze.hs +++ b/src/App/Fossa/Config/SBOM/Analyze.hs @@ -59,6 +59,7 @@ data SBOMAnalyzeConfig = SBOMAnalyzeConfig , sbomPath :: SBOMFile , sbomRebuild :: DependencyRebuild , sbomTeam :: Maybe Text + , sbomOutputJson :: Flag JsonOutput , sbomRevision :: ProjectRevision , debugDir :: Maybe FilePath , severity :: Severity @@ -72,6 +73,7 @@ data SBOMAnalyzeOptions = SBOMAnalyzeOptions { analyzeCommons :: App.Fossa.Config.Common.CommonOpts , team :: Maybe Text , forceRescan :: Flag ForceRescan + , jsonOutput :: Flag JsonOutput , sbomFile :: SBOMFile } @@ -94,6 +96,7 @@ cliParser = <$> App.Fossa.Config.Common.commonOpts <*> optional (strOption (applyFossaStyle <> long "team" <> short 'T' <> stringToHelpDoc "This SBOM's team inside your organization")) <*> flagOpt ForceRescan (applyFossaStyle <> long "force-rescan" <> stringToHelpDoc "Force sbom file to be rescanned even if the revision has been previously analyzed by FOSSA.") + <*> flagOpt JsonOutput (applyFossaStyle <> long "json" <> stringToHelpDoc "Output project metadata as JSON to the console. This is useful for communicating with the FOSSA API.") <*> sbomFileArg mergeOpts :: @@ -129,6 +132,7 @@ mergeOpts maybeDebugDir cfgfile envvars cliOpts@SBOMAnalyzeOptions{..} = do , severity = severity , sbomRebuild = forceRescans , sbomTeam = team + , sbomOutputJson = jsonOutput , sbomRevision = revision , debugDir = maybeDebugDir } diff --git a/src/App/Fossa/SBOM/Analyze.hs b/src/App/Fossa/SBOM/Analyze.hs index 24a42fd273..7d11b86349 100644 --- a/src/App/Fossa/SBOM/Analyze.hs +++ b/src/App/Fossa/SBOM/Analyze.hs @@ -4,7 +4,9 @@ module App.Fossa.SBOM.Analyze ( ) where import App.Fossa.API.BuildLink (getFossaBuildUrl) +import App.Fossa.Analyze.Upload (buildProjectSummary) import App.Fossa.Config.SBOM +import App.Fossa.Config.SBOM.Analyze (JsonOutput (JsonOutput)) import App.Fossa.PreflightChecks (PreflightCommandChecks (..), preflightChecks) import App.Types (ComponentUploadFileType (..), ProjectMetadata (..), ProjectRevision (..)) import Control.Carrier.Debug (Debug) @@ -12,14 +14,17 @@ import Control.Carrier.Diagnostics qualified as Diag import Control.Carrier.FossaApiClient (runFossaApiClient) import Control.Carrier.StickyLogger (StickyLogger, logSticky, runStickyLogger) import Control.Carrier.Telemetry.Types (CountableCliFeature (SBOMAnalyzeUsage)) +import Control.Effect.Diagnostics (context) import Control.Effect.FossaApiClient (FossaApiClient, PackageRevision (PackageRevision), getOrganization, getSignedUploadUrl, queueSBOMBuild, uploadArchive) import Control.Effect.Lift import Control.Effect.Telemetry (Telemetry, trackUsage) -import Control.Monad (void) +import Control.Monad (void, when) +import Data.Aeson qualified as Aeson +import Data.Flag (fromFlag) import Data.Foldable (traverse_) import Data.String.Conversion (ConvertUtf8 (..), toString, toText) import Data.Text (Text) -import Effect.Logger (Logger, logDebug, logInfo) +import Effect.Logger (Logger, logDebug, logInfo, logStdout) import Fossa.API.Types import Prettyprinter (Pretty (pretty)) import Srclib.Types (Locator (..)) @@ -45,6 +50,7 @@ analyzeInternal :: , Has StickyLogger sig m , Has Logger sig m , Has FossaApiClient sig m + , Has (Lift IO) sig m ) => SBOMAnalyzeConfig -> m () @@ -75,6 +81,12 @@ analyzeInternal config = do , "============================================================" ] + when (fromFlag JsonOutput $ sbomOutputJson config) $ do + summary <- + context "Analysis ran successfully, but the server returned invalid metadata" $ + buildProjectSummary revision locator buildUrl + logStdout . decodeUtf8 $ Aeson.encode summary + uploadSBOM :: ( Has StickyLogger sig m , Has FossaApiClient sig m diff --git a/test/App/Fossa/SBOMAnalyzeSpec.hs b/test/App/Fossa/SBOMAnalyzeSpec.hs index 48059f6d0f..95cbf97d54 100644 --- a/test/App/Fossa/SBOMAnalyzeSpec.hs +++ b/test/App/Fossa/SBOMAnalyzeSpec.hs @@ -1,17 +1,19 @@ module App.Fossa.SBOMAnalyzeSpec (spec) where -import App.Fossa.Config.SBOM.Analyze (SBOMAnalyzeConfig (..)) +import App.Fossa.Config.SBOM.Analyze (JsonOutput (JsonOutput), SBOMAnalyzeConfig (..), SBOMAnalyzeOptions (..), cliParser) import App.Fossa.Config.SBOM.Common (SBOMFile (..)) +import App.Fossa.Config.Utils (parseArgString) import App.Fossa.SBOM.Analyze (analyzeInternal) import App.Types (BaseDir (..), ComponentUploadFileType (..), DependencyRebuild (DependencyRebuildReuseCache), ProjectRevision (ProjectRevision)) import Control.Algebra (Has) import Control.Carrier.Debug (ignoreDebug) import Control.Carrier.Telemetry (withoutTelemetry) import Control.Effect.FossaApiClient (FossaApiClientF (..), PackageRevision (..)) +import Data.Flag (fromFlag, toFlag') import Effect.Logger (Severity (SevInfo)) import Fossa.API.Types (Archive (..)) import Path.IO (getCurrentDir) -import Test.Effect (it') +import Test.Effect (it', shouldBe') import Test.Fixtures qualified as Fixtures import Test.Hspec (Spec, describe, runIO) import Test.MockApi (MockApi, alwaysReturns, returnsOnce, returnsOnceForAnyRequest, runMockApi) @@ -23,7 +25,7 @@ spec = do it' "should upload a file" $ do let archive = Archive "somesbom" "1.2.3" Nothing Nothing let revision = ProjectRevision "somesbom" "1.2.3" Nothing - let config = SBOMAnalyzeConfig (BaseDir currDir) Fixtures.apiOpts (SBOMFile "test/App/Fossa/SBOM/testdata/sampleCycloneDX.json") DependencyRebuildReuseCache Nothing revision Nothing SevInfo + let config = SBOMAnalyzeConfig (BaseDir currDir) Fixtures.apiOpts (SBOMFile "test/App/Fossa/SBOM/testdata/sampleCycloneDX.json") DependencyRebuildReuseCache Nothing (toFlag' False) revision Nothing SevInfo GetApiOpts `alwaysReturns` Fixtures.apiOpts expectOrganization @@ -33,6 +35,15 @@ spec = do ignoreDebug . withoutTelemetry . runMockApi $ analyzeInternal config + describe "SBOM Analyze cliParser" $ do + it' "should default --json off" $ do + opts <- parseArgString cliParser "test/App/Fossa/SBOM/testdata/sampleCycloneDX.json" + fromFlag JsonOutput (jsonOutput opts) `shouldBe'` False + + it' "should parse --json into the options" $ do + opts <- parseArgString cliParser "--json test/App/Fossa/SBOM/testdata/sampleCycloneDX.json" + fromFlag JsonOutput (jsonOutput opts) `shouldBe'` True + expectOrganization :: Has MockApi sig m => m () expectOrganization = GetOrganization `alwaysReturns` Fixtures.organization From 9a98e2fbe8600b6780153ee4a3134c253f85500a Mon Sep 17 00:00:00 2001 From: Zachary LaVallee Date: Tue, 28 Jul 2026 09:50:27 -0700 Subject: [PATCH 2/2] docs: fill in changelog PR link (#1736) Co-Authored-By: Claude Opus 4.8 --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 1b14eb5997..8008a59ffe 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,7 +2,7 @@ ## Unreleased -- SBOM: `fossa sbom analyze --json` prints project metadata as JSON, matching `fossa analyze --json`. ([#XXXX](https://github.com/fossas/fossa-cli/pull/XXXX)) +- SBOM: `fossa sbom analyze --json` prints project metadata as JSON, matching `fossa analyze --json`. ([#1736](https://github.com/fossas/fossa-cli/pull/1736)) ## 3.17.15