diff --git a/Changelog.md b/Changelog.md index 4ccb1bdb9a..8008a59ffe 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`. ([#1736](https://github.com/fossas/fossa-cli/pull/1736)) + ## 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