Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
13 changes: 13 additions & 0 deletions docs/references/subcommands/sbom.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+<org-id>/<name>`.

```sh
fossa sbom analyze /path/to/sampleCycloneDX.json --json
```

```json
{"branch":null, "id":"sbom+<org-id>/sampleCycloneDX$123", "project":"<org-id>/sampleCycloneDX", "projectId":"sbom+<org-id>/sampleCycloneDX", "revision":"123", "url":"https://app.fossa.com/projects/sbom%2b<org-id>%2fsampleCycloneDX/refs/branch/master/123"}
```

### Team Permissions

Expand Down
1 change: 1 addition & 0 deletions src/App/Fossa/Analyze/Upload.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module App.Fossa.Analyze.Upload (
mergeSourceAndLicenseUnits,
uploadSuccessfulAnalysis,
buildProjectSummary,
emitBuildWarnings,
ScanUnits (..),
) where
Expand Down
4 changes: 4 additions & 0 deletions src/App/Fossa/Config/SBOM/Analyze.hs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ data SBOMAnalyzeConfig = SBOMAnalyzeConfig
, sbomPath :: SBOMFile
, sbomRebuild :: DependencyRebuild
, sbomTeam :: Maybe Text
, sbomOutputJson :: Flag JsonOutput
, sbomRevision :: ProjectRevision
, debugDir :: Maybe FilePath
, severity :: Severity
Expand All @@ -72,6 +73,7 @@ data SBOMAnalyzeOptions = SBOMAnalyzeOptions
{ analyzeCommons :: App.Fossa.Config.Common.CommonOpts
, team :: Maybe Text
, forceRescan :: Flag ForceRescan
, jsonOutput :: Flag JsonOutput
, sbomFile :: SBOMFile
}

Expand All @@ -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 ::
Expand Down Expand Up @@ -129,6 +132,7 @@ mergeOpts maybeDebugDir cfgfile envvars cliOpts@SBOMAnalyzeOptions{..} = do
, severity = severity
, sbomRebuild = forceRescans
, sbomTeam = team
, sbomOutputJson = jsonOutput
, sbomRevision = revision
, debugDir = maybeDebugDir
}
16 changes: 14 additions & 2 deletions src/App/Fossa/SBOM/Analyze.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@ 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)
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 (..))
Expand All @@ -45,6 +50,7 @@ analyzeInternal ::
, Has StickyLogger sig m
, Has Logger sig m
, Has FossaApiClient sig m
, Has (Lift IO) sig m
) =>
SBOMAnalyzeConfig ->
m ()
Expand Down Expand Up @@ -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
Expand Down
17 changes: 14 additions & 3 deletions test/App/Fossa/SBOMAnalyzeSpec.hs
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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
Expand All @@ -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

Expand Down
Loading