Skip to content

Commit 99c8247

Browse files
authored
SBOM report ane 2277 (#1534)
* Add a force-sbom option. * Fetch SBOMs using fossa report. * Implement tests,do error reporting better. * Make report intelligently pick between bases. * Just use the fallback method if we ever get a directory. * Fix tests * Try to make fixtures work on windows too. * cleanup * Update docs * Update changelog. * Delete redundant import. * Parse fix. * Specify that you need to run `fossa sbom analyze` first.
1 parent 8bb8c62 commit 99c8247

File tree

16 files changed

+382
-147
lines changed

16 files changed

+382
-147
lines changed

Changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# FOSSA CLI Changelog
22

3+
## 3.10.7
4+
5+
- Report: Allow generating SBOMs attribution reports using fossa-cli. ([#1534](https://github.com/fossas/fossa-cli/pull/1534))
6+
37
## 3.10.6
48
- Licensing: Fix a bug where the scikit-learn had an incorrect license detected ([#1527](https://github.com/fossas/fossa-cli/pull/1527))
59
- Licensing: Adds support for the NREL disclaimer

docs/references/subcommands/report.md

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,41 @@ fossa report attribution --timeout 60
2424

2525
Where `60` is the maximum number of seconds to wait for the report to be downloaded.
2626

27+
### Valid Report Targets
28+
29+
#### Project Directory
30+
31+
You can specify a project directly like you would with `fossa analyze` to generate a report.
32+
For example:
33+
34+
```
35+
fossa report attribution --format json ~/my-project
36+
```
37+
38+
With no final path, FOSSA will try to fetch a report for the current directory's project.
39+
40+
#### SBOM Files
41+
42+
After using [`fossa sbom analyze`](./sbom.md), you can specify an SBOM that you would like to generate a report for using its file:
43+
44+
```
45+
fossa report attribution --format json ~/my-project-sbom.txt
46+
```
47+
48+
#### Project Arguments
49+
50+
All `fossa` commands support the following FOSSA-project-related flags:
51+
52+
| Name | Short | Description |
53+
| ---------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
54+
| `--project 'some project'` | `-p` | Override the detected project name |
55+
| `--revision 'some revision'` | `-r` | -Override the detected project revision |
56+
| `--fossa-api-key 'my-api-key'` | | An alternative to using the `FOSSA_API_KEY` environment variable to specify a FOSSA API key |
57+
| `--endpoint 'https://example.com'` | `-e` | Override the FOSSA API server base URL |
58+
| `--config /path/to/file` | `-c` | Path to a [configuration file](../files/fossa-yml.md) including filename. By default we look for `.fossa.yml` in base working directory. |
59+
60+
In this case, FOSSA will attempt to fetch any report it can find matching the `project` and `revision` criteria.
61+
2762
### Specifying a report format
2863

2964
`fossa report` supports customizing the format used to render a report via the `--format` flag.
@@ -59,15 +94,3 @@ To use this compatibility script:
5994
2. Run `fossa report attribution --format json`, piping its output to `compat-attribution`.
6095
For example, `fossa report attribution --format json | compat-attribution`
6196
3. Parse the resulting output as you would have from FOSSAv1.
62-
63-
## Common FOSSA Project Flags
64-
65-
All `fossa` commands support the following FOSSA-project-related flags:
66-
67-
| Name | Short | Description |
68-
| ---------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
69-
| `--project 'some project'` | `-p` | Override the detected project name |
70-
| `--revision 'some revision'` | `-r` | -Override the detected project revision |
71-
| `--fossa-api-key 'my-api-key'` | | An alternative to using the `FOSSA_API_KEY` environment variable to specify a FOSSA API key |
72-
| `--endpoint 'https://example.com'` | `-e` | Override the FOSSA API server base URL |
73-
| `--config /path/to/file` | `-c` | Path to a [configuration file](../files/fossa-yml.md) including filename. By default we look for `.fossa.yml` in base working directory. |

src/App/Fossa/API/BuildWait.hs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,14 @@ waitForReportReadiness ::
153153
) =>
154154
ProjectRevision ->
155155
Cancel ->
156+
LocatorType ->
156157
m ()
157-
waitForReportReadiness revision cancelFlag = do
158-
void $ waitForIssues revision Nothing LocatorTypeCustom cancelFlag
158+
waitForReportReadiness revision cancelFlag locatorType = do
159+
void $ waitForIssues revision Nothing locatorType cancelFlag
159160

160161
supportsDepCacheReadinessPolling <- orgSupportsDependenciesCachePolling <$> getOrganization
161162
when supportsDepCacheReadinessPolling $
162-
waitForValidDependenciesCache revision cancelFlag
163+
waitForValidDependenciesCache revision cancelFlag locatorType
163164

164165
waitForValidDependenciesCache ::
165166
( Has Diagnostics sig m
@@ -170,15 +171,16 @@ waitForValidDependenciesCache ::
170171
) =>
171172
ProjectRevision ->
172173
Cancel ->
174+
LocatorType ->
173175
m ()
174-
waitForValidDependenciesCache revision cancelFlag = do
176+
waitForValidDependenciesCache revision cancelFlag locatorType = do
175177
checkForTimeout cancelFlag
176-
cacheStatus <- getRevisionDependencyCacheStatus revision
178+
cacheStatus <- getRevisionDependencyCacheStatus revision locatorType
177179

178180
case status cacheStatus of
179181
Ready -> pure ()
180182
Waiting -> do
181183
logSticky' $ "[ Waiting for revision's dependency cache... last status: " <> viaShow Waiting <> " ]"
182184
pauseForRetry
183-
waitForValidDependenciesCache revision cancelFlag
185+
waitForValidDependenciesCache revision cancelFlag locatorType
184186
UnknownDependencyCacheStatus status -> fatalText $ "unknown status of " <> status <> " received for revision's dependency cache"

src/App/Fossa/Config/Common.hs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ module App.Fossa.Config.Common (
4848
titleHelp,
4949
-- Deprecation
5050
applyReleaseGroupDeprecationWarning,
51+
collectBaseFile,
5152
) where
5253

5354
import App.Fossa.Config.ConfigFile (
@@ -259,6 +260,8 @@ pathOpt = first show . parseRelDir
259260
targetOpt :: String -> Either String TargetFilter
260261
targetOpt = first errorBundlePretty . runParser targetFilterParser "(Command-line arguments)" . toText
261262

263+
-- | Argument for the base dir for FOSSA to operate out of.
264+
-- Defaults to the current directory, ".".
262265
baseDirArg :: Parser String
263266
baseDirArg = argument str (applyFossaStyle <> metavar "DIR" <> helpDoc baseDirDoc <> value ".")
264267
where
@@ -278,6 +281,14 @@ collectBaseDir ::
278281
m BaseDir
279282
collectBaseDir = fmap BaseDir . validateDir
280283

284+
collectBaseFile ::
285+
( Has Diagnostics sig m
286+
, Has (Lift IO) sig m
287+
, Has ReadFS sig m
288+
) =>
289+
FilePath -> m (Path Abs File)
290+
collectBaseFile = validateFile
291+
281292
validateDir ::
282293
( Has Diagnostics sig m
283294
, Has (Lift IO) sig m

src/App/Fossa/Config/Report.hs

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module App.Fossa.Config.Report (
55
ReportCliOptions,
66
ReportOutputFormat (..),
77
ReportType (..),
8+
ReportBase (..),
89
mkSubCommand,
910
-- Exported for testing
1011
parseReportOutputFormat,
@@ -16,21 +17,27 @@ import App.Fossa.Config.Common (
1617
baseDirArg,
1718
collectApiOpts,
1819
collectBaseDir,
20+
collectBaseFile,
1921
collectRevisionData',
2022
commonOpts,
2123
defaultTimeoutDuration,
2224
)
2325
import App.Fossa.Config.ConfigFile (ConfigFile, resolveLocalConfigFile)
2426
import App.Fossa.Config.EnvironmentVars (EnvVars)
27+
import App.Fossa.Config.SBOM.Common qualified as SBOMCfg
2528
import App.Fossa.Subcommand (EffStack, GetCommonOpts (getCommonOpts), GetSeverity (getSeverity), SubCommand (SubCommand))
26-
import App.Types (BaseDir, OverrideProject (OverrideProject), ProjectRevision)
29+
import App.Types (BaseDir (..), OverrideProject (OverrideProject), ProjectRevision)
30+
import Control.Applicative ((<|>))
2731
import Control.Effect.Diagnostics (Diagnostics, ToDiagnostic (renderDiagnostic), errHelp, fatal, fromMaybe)
32+
import Control.Effect.Diagnostics qualified as Diag
2833
import Control.Effect.Lift (Has, Lift)
2934
import Control.Timeout (Duration (Seconds))
3035
import Data.Aeson (ToJSON (toEncoding), defaultOptions, genericToEncoding)
3136
import Data.Error (SourceLocation, createEmptyBlock, getSourceLocation)
3237
import Data.List (intercalate)
3338
import Data.String.Conversion (ToText, toText)
39+
import Data.String.Conversion qualified as Conv
40+
import Data.Text (Text)
3441
import Effect.Exec (Exec)
3542
import Effect.Logger (Logger, Severity (..), vsep)
3643
import Effect.ReadFS (ReadFS)
@@ -52,6 +59,7 @@ import Options.Applicative (
5259
switch,
5360
)
5461
import Options.Applicative.Builder (helpDoc)
62+
import Path
5563
import Prettyprinter (Doc, comma, hardline, punctuate, viaShow)
5664
import Prettyprinter.Render.Terminal (AnsiStyle, Color (Green, Red))
5765
import Style (applyFossaStyle, boldItalicized, coloredBoldItalicized, formatDoc, stringToHelpDoc, styledDivider)
@@ -146,8 +154,11 @@ parser =
146154
<*> optional (strOption (applyFossaStyle <> long "format" <> helpDoc formatHelp))
147155
<*> optional (option auto (applyFossaStyle <> long "timeout" <> stringToHelpDoc "Duration to wait for build completion (in seconds)"))
148156
<*> reportTypeArg
149-
<*> baseDirArg
157+
<*> basePath
150158
where
159+
basePath :: Parser Text
160+
basePath = (Conv.toText <$> baseDirArg) <|> (SBOMCfg.unSBOMFile <$> SBOMCfg.sbomFileArg)
161+
151162
jsonHelp :: Maybe (Doc AnsiStyle)
152163
jsonHelp =
153164
Just . formatDoc $
@@ -178,7 +189,7 @@ data ReportCliOptions = ReportCliOptions
178189
, cliReportOutputFormat :: Maybe String
179190
, cliReportTimeout :: Maybe Int
180191
, cliReportType :: ReportType
181-
, cliReportBaseDir :: FilePath
192+
, cliReportBase :: Text
182193
}
183194
deriving (Eq, Ord, Show)
184195

@@ -211,19 +222,43 @@ mergeOpts ::
211222
m ReportConfig
212223
mergeOpts cfgfile envvars ReportCliOptions{..} = do
213224
let apiOpts = collectApiOpts cfgfile envvars commons
214-
basedir = collectBaseDir cliReportBaseDir
215225
outputformat = validateOutputFormat cliReportJsonOutput cliReportOutputFormat
216226
timeoutduration = maybe defaultTimeoutDuration Seconds cliReportTimeout
217-
revision =
218-
collectRevisionData' basedir cfgfile ReadOnly $
219-
OverrideProject (optProjectName commons) (optProjectRevision commons) Nothing
227+
projectOverride = OverrideProject (optProjectName commons) (optProjectRevision commons) Nothing
228+
229+
(revision, reportBase) <- generateDirOrSBOMBase cliReportBase projectOverride
230+
220231
ReportConfig
221232
<$> apiOpts
222-
<*> basedir
233+
<*> pure reportBase
223234
<*> outputformat
224235
<*> pure timeoutduration
225236
<*> pure cliReportType
226-
<*> revision
237+
<*> pure revision
238+
where
239+
generateDirOrSBOMBase ::
240+
( Has Exec sig m
241+
, Has Logger sig m
242+
, Has (Lift IO) sig m
243+
, Has ReadFS sig m
244+
, Has Diagnostics sig m
245+
) =>
246+
Text -> OverrideProject -> m (ProjectRevision, ReportBase)
247+
generateDirOrSBOMBase path projectOverride = do
248+
basedir <- Diag.recover $ collectBaseDir (Conv.toString path)
249+
case basedir of
250+
Just dir ->
251+
(,)
252+
<$> collectRevisionData' (pure dir) cfgfile ReadOnly projectOverride
253+
<*> pure (DirectoryBase . unBaseDir $ dir)
254+
Nothing -> do
255+
baseFile <- Diag.recover $ collectBaseFile (Conv.toString path)
256+
case baseFile of
257+
Just file ->
258+
(,)
259+
<$> SBOMCfg.getProjectRevision (SBOMCfg.SBOMFile (toText file)) projectOverride ReadOnly
260+
<*> pure (SBOMBase file)
261+
Nothing -> Diag.fatalText $ "No such file or directory " <> path
227262

228263
newtype NoFormatProvided = NoFormatProvided SourceLocation
229264
instance ToDiagnostic NoFormatProvided where
@@ -252,13 +287,21 @@ validateOutputFormat False (Just format) = errHelp ReportErrorHelp $ fromMaybe (
252287

253288
data ReportConfig = ReportConfig
254289
{ apiOpts :: ApiOpts
255-
, baseDir :: BaseDir
290+
, reportBase :: ReportBase
256291
, outputFormat :: ReportOutputFormat
257292
, timeoutDuration :: Duration
258293
, reportType :: ReportType
259294
, revision :: ProjectRevision
260295
}
261296
deriving (Eq, Ord, Show, Generic)
262297

298+
data ReportBase
299+
= SBOMBase (Path Abs File)
300+
| DirectoryBase (Path Abs Dir)
301+
deriving (Eq, Ord, Show, Generic)
302+
303+
instance ToJSON ReportBase where
304+
toEncoding = genericToEncoding defaultOptions
305+
263306
instance ToJSON ReportConfig where
264307
toEncoding = genericToEncoding defaultOptions

src/App/Fossa/Config/SBOM/Common.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,4 @@ getProjectRevision sbomPath override cacheStrategy = do
6666
let version = fromMaybe inferredVersion $ overrideRevision override
6767
let revision = ProjectRevision name version Nothing
6868
when (cacheStrategy == WriteOnly) $ saveRevision revision
69-
pure $ ProjectRevision name version Nothing
69+
pure revision

src/App/Fossa/Container/Scan.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import App.Fossa.Container.Sources.DockerArchive (analyzeFromDockerArchive, revi
1616
import App.Fossa.Container.Sources.DockerEngine (analyzeFromDockerEngine, revisionFromDockerEngine)
1717
import App.Fossa.Container.Sources.Podman (analyzeFromPodman, podmanInspectImage, revisionFromPodman)
1818
import App.Fossa.Container.Sources.Registry (analyzeFromRegistry, revisionFromRegistry, runWithCirceReexport)
19-
import App.Types (OverrideProject (..), ProjectRevision (ProjectRevision))
19+
import App.Types (OverrideProject (..), ProjectRevision (..))
2020
import Container.Docker.SourceParser (RegistryImageSource (..), parseImageUrl)
2121
import Container.Types (ContainerScan (..))
2222
import Control.Carrier.DockerEngineApi (runDockerEngineApi)

src/App/Fossa/Report.hs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ import App.Fossa.API.BuildWait (
1010
waitForReportReadiness,
1111
waitForScanCompletion,
1212
)
13-
import App.Fossa.Config.Report (ReportCliOptions, ReportConfig (..), ReportOutputFormat (ReportJson), mkSubCommand)
13+
import App.Fossa.Config.Report (ReportBase (..), ReportCliOptions, ReportConfig (..), ReportOutputFormat (ReportJson), mkSubCommand)
1414
import App.Fossa.PreflightChecks (PreflightCommandChecks (ReportChecks), guardWithPreflightChecks)
1515
import App.Fossa.Subcommand (SubCommand)
1616
import App.Types (LocatorType (..), ProjectRevision (..))
1717
import Control.Carrier.Debug (ignoreDebug)
1818
import Control.Carrier.FossaApiClient (runFossaApiClient)
1919
import Control.Carrier.StickyLogger (StickyLogger, logSticky, runStickyLogger)
20-
import Control.Effect.Diagnostics (Diagnostics)
20+
import Control.Effect.Diagnostics (Diagnostics, (<||>))
2121
import Control.Effect.FossaApiClient (FossaApiClient, getAttribution)
2222
import Control.Effect.Lift (Has, Lift)
2323
import Control.Monad (void, when)
2424
import Control.Timeout (timeout')
25+
import Data.Functor (($>))
2526
import Data.String.Conversion (toText)
2627
import Data.Text.Extra (showT)
2728
import Effect.Logger (
@@ -79,13 +80,20 @@ fetchReport ReportConfig{..} =
7980
when (outputFormat /= ReportJson) $
8081
logWarn (pretty $ "\"" <> toText outputFormat <> "\" format may change independent of CLI version: it is sourced from the FOSSA service.")
8182

83+
let waitForSbomCompletion = waitForScanCompletion revision LocatorTypeSBOM cancelToken $> LocatorTypeSBOM
84+
waitForCustomCompletion = waitForScanCompletion revision LocatorTypeCustom cancelToken $> LocatorTypeCustom
85+
8286
logSticky "[ Waiting for build completion... ]"
87+
locatorType <-
88+
case reportBase of
89+
SBOMBase _ -> waitForSbomCompletion
90+
DirectoryBase _ -> waitForCustomCompletion <||> waitForSbomCompletion
8391

84-
waitForScanCompletion revision LocatorTypeCustom cancelToken
8592
logSticky "[ Waiting for scan completion... ]"
8693

87-
waitForReportReadiness revision cancelToken
94+
waitForReportReadiness revision cancelToken locatorType
95+
8896
logSticky $ "[ Fetching " <> showT reportType <> " report... ]"
8997

90-
renderedReport <- getAttribution revision outputFormat
98+
renderedReport <- getAttribution revision outputFormat locatorType
9199
logStdout renderedReport

src/App/Types.hs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ module App.Types (
1919
ComponentUploadFileType (..),
2020
Mode (..),
2121
uploadFileTypeToFetcherName,
22+
locatorAPIText,
2223
) where
2324

2425
import Data.Aeson (FromJSON (parseJSON), ToJSON (toEncoding), defaultOptions, genericToEncoding, object, withObject, (.:), (.=))
2526
import Data.Aeson.Types (toJSON)
2627
import Data.Map (Map)
28+
import Data.String (IsString)
2729
import Data.String.Conversion (ToText (..), showText)
2830
import Data.Text (Text)
2931
import DepTypes (DepType)
@@ -92,13 +94,16 @@ data ProjectRevision = ProjectRevision
9294
data LocatorType = LocatorTypeCustom | LocatorTypeSBOM
9395
deriving (Eq, Ord, Show, Generic)
9496

97+
-- | How to output a 'LocatorType' when interacting with FOSSA APIs.
98+
locatorAPIText :: IsString a => LocatorType -> a
99+
locatorAPIText LocatorTypeCustom = "custom"
100+
locatorAPIText LocatorTypeSBOM = "sbom"
101+
95102
instance ToText LocatorType where
96-
toText LocatorTypeCustom = "custom"
97-
toText LocatorTypeSBOM = "sbom"
103+
toText = locatorAPIText
98104

99105
instance ToJSON LocatorType where
100-
toEncoding LocatorTypeCustom = "custom"
101-
toEncoding LocatorTypeSBOM = "sbom"
106+
toEncoding = locatorAPIText
102107

103108
instance ToJSON ProjectRevision where
104109
toEncoding = genericToEncoding defaultOptions

src/Control/Carrier/FossaApiClient.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ runFossaApiClient apiOpts action = do
6666
FinalizeLicenseScan components -> LicenseScanning.finalizeLicenseScan components
6767
FinalizeLicenseScanForPathDependency locators forceRebuild -> LicenseScanning.finalizePathDependencyScan locators forceRebuild
6868
GetApiOpts -> pure apiOpts
69-
GetAttribution rev format -> Core.getAttribution rev format
69+
GetAttribution rev format locType -> Core.getAttribution rev format locType
7070
GetIssues rev diffRev locatorType -> Core.getIssues rev diffRev locatorType
7171
GetEndpointVersion -> Core.getEndpointVersion
7272
GetLatestBuild rev locatorType -> Core.getLatestBuild rev locatorType
73-
GetRevisionDependencyCacheStatus rev -> Core.getRevisionDependencyCacheStatus rev
73+
GetRevisionDependencyCacheStatus rev locatorType -> Core.getRevisionDependencyCacheStatus rev locatorType
7474
GetOrganization -> Core.getOrganization
7575
GetPolicies -> Core.getPolicies
7676
GetProject rev locatorType -> Core.getProject rev locatorType

0 commit comments

Comments
 (0)