Skip to content

Commit 5116f00

Browse files
authored
Fork aliasing (#1620)
1 parent e06faa0 commit 5116f00

16 files changed

Lines changed: 1011 additions & 68 deletions

File tree

Changelog.md

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

3+
## 3.14.1
4+
- Add fork-aliasing. Use this if you are using a fork of a dependency, but want FOSSA to treat it as if you were using the base version that you forked from. ([#1620](https://github.com/fossas/fossa-cli/pull/1620))
5+
36
## 3.14.0
47
- Adds `--x-vendetta` flag for vendored dependency identification ([#1607](https://github.com/fossas/fossa-cli/pull/1607))
58

docs/references/files/fossa-deps.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,60 @@ vendored-dependencies:
9191

9292
For more details, please refer to the [feature](../../features/vendored-dependencies.md) walk through.
9393

94+
### `fork-aliases:`
95+
96+
Denotes mapping of fork dependencies to their base dependencies. This is useful when you have forked a dependency and want it to be treated as the base dependency by FOSSA. This, for example, will allow FOSSA to find and report security issues that are associated with the root project.
97+
98+
As an example, if you have forked the Serde crate in a private repository and called it `my-serde`, then you could tell FOSSA to tranlate `my-serde` to `serde` like this:
99+
100+
```yaml
101+
fork-aliases:
102+
- fork:
103+
type: cargo
104+
name: my-serde
105+
base:
106+
type: cargo
107+
name: serde
108+
```
109+
110+
- `fork`: The fork dependency entry that should be aliased to the base dependency. (Required)
111+
- `type`: Type of the fork dependency. (Required)
112+
- `name`: Name of the fork dependency. (Required)
113+
- `version`: Version of the fork dependency. See "version matching rules" below for more details. (Optional)
114+
- `base`: The base dependency that your fork should be aliased to. (Required)
115+
- `type`: Type of the base dependency. (Required)
116+
- `name`: Name of the base dependency. (Required)
117+
- `version`: Version of the base dependency. See "version matching rules" below for more details. (Optional)
118+
- `labels`: An optional list of labels to be added to the fork alias. The dependency in FOSSA's UI will be treated as a normal dependency, so if you use an `org` scope for the label it will be applied to all versions of the base dependency across your organization. We suggest using the `project` or `revision` scope for labels on fork aliases.
119+
120+
**Version Matching rules:**
121+
- If `fork` version is specified, only that exact version will be translated
122+
- If `fork` version is not specified, any version will be translated to the base dependency. The version of the translated dependency depends on whether the base version is specified or not.
123+
124+
- If `base` version is specified, the dependency will always be translated to the specified version
125+
- If `base` version is not specified, the original version from the fork is preserved
126+
127+
```yaml
128+
fork-aliases:
129+
- fork:
130+
type: cargo
131+
name: my-serde
132+
base:
133+
type: cargo
134+
name: serde
135+
labels:
136+
- label: internal
137+
scope: project
138+
- fork:
139+
type: cargo
140+
name: my-serde
141+
version: 1.0.0 # Only version 1.0.0 will be translated
142+
base:
143+
type: cargo
144+
name: serde
145+
version: 2.0.0 # Will always translate to version 2.0.0
146+
```
147+
94148
## Labels
95149

96150
Each kind of dependency referenced above can have a `labels` field, which is a list of labels to be added to the dependency.
@@ -140,6 +194,19 @@ vendored-dependencies:
140194
scope: project
141195
- label: internal-dependency
142196
scope: revision
197+
198+
fork-aliases:
199+
- fork:
200+
type: cargo
201+
name: my-serde
202+
base:
203+
type: cargo
204+
name: serde
205+
labels:
206+
- label: internal
207+
scope: org
208+
- label: fork-approved
209+
scope: revision
143210
```
144211

145212
## Errors in the `fossa-deps` file

docs/references/files/fossa-deps.schema.json

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,31 @@
4242
],
4343
"description": "Name of the distribution OS."
4444
},
45+
"dependency-type": {
46+
"enum": [
47+
"bower",
48+
"cargo",
49+
"carthage",
50+
"composer",
51+
"cpan",
52+
"renv",
53+
"gem",
54+
"git",
55+
"go",
56+
"hackage",
57+
"hex",
58+
"maven",
59+
"npm",
60+
"nuget",
61+
"paket",
62+
"pub",
63+
"pypi",
64+
"cocoapods",
65+
"swift",
66+
"url"
67+
],
68+
"description": "Type of the dependency. It informs FOSSA which relevant registries to search for dependency's distribution."
69+
},
4570
"referenced-app-dependency": {
4671
"properties": {
4772
"name": {
@@ -50,29 +75,7 @@
5075
"description": "Name of the dependency. This name will be used to search for dependency in relevant registries."
5176
},
5277
"type": {
53-
"enum": [
54-
"bower",
55-
"cargo",
56-
"carthage",
57-
"composer",
58-
"cpan",
59-
"renv",
60-
"gem",
61-
"git",
62-
"go",
63-
"hackage",
64-
"hex",
65-
"maven",
66-
"npm",
67-
"nuget",
68-
"paket",
69-
"pub",
70-
"pypi",
71-
"cocoapods",
72-
"swift",
73-
"url"
74-
],
75-
"description": "Type of the dependency. It informs FOSSA which relevant registries to search for dependency's distribution."
78+
"$ref": "#/$defs/dependency-type"
7679
},
7780
"version": {
7881
"type": "string",
@@ -330,6 +333,51 @@
330333
"version"
331334
],
332335
"additionalProperties": false
336+
},
337+
"fork-alias-entry": {
338+
"properties": {
339+
"type": {
340+
"$ref": "#/$defs/dependency-type"
341+
},
342+
"name": {
343+
"type": "string",
344+
"minLength": 1,
345+
"description": "Name of the dependency. This name will be used to search for dependency in relevant registries."
346+
},
347+
"version": {
348+
"type": "string",
349+
"description": "Version of the dependency. It informs FOSSA which version of the dependency to scan. Optional. See [fork aliases documentation](./fossa-deps.md#fork-aliases) for more information."
350+
}
351+
},
352+
"required": [
353+
"type",
354+
"name"
355+
],
356+
"additionalProperties": false
357+
},
358+
"fork-alias": {
359+
"properties": {
360+
"fork": {
361+
"$ref": "#/$defs/fork-alias-entry",
362+
"description": "The fork dependency entry that should be aliased to the base dependency."
363+
},
364+
"base": {
365+
"$ref": "#/$defs/fork-alias-entry",
366+
"description": "The base/original dependency entry that your fork should be aliased to."
367+
},
368+
"labels": {
369+
"type": "array",
370+
"description": "Optional labels to be applied to the fork alias.",
371+
"items": {
372+
"$ref": "#/$defs/label"
373+
}
374+
}
375+
},
376+
"required": [
377+
"fork",
378+
"base"
379+
],
380+
"additionalProperties": false
333381
}
334382
},
335383
"type": "object",
@@ -364,6 +412,13 @@
364412
"items": {
365413
"$ref": "#/$defs/remote-dependency"
366414
}
415+
},
416+
"fork-aliases": {
417+
"type": "array",
418+
"description": "Fork aliases to map your fork dependencies to their base dependencies. Matching: if fork version is specified, only that exact version matches; if not specified, any version matches. Translation: if base version is specified, always use that version; if not specified, preserve the original version.",
419+
"items": {
420+
"$ref": "#/$defs/fork-alias"
421+
}
367422
}
368423
},
369424
"required": []

spectrometer.cabal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ library
187187
App.Fossa.Analyze.Debug
188188
App.Fossa.Analyze.Discover
189189
App.Fossa.Analyze.Filter
190+
App.Fossa.Analyze.ForkAlias
190191
App.Fossa.Analyze.Graph
191192
App.Fossa.Analyze.GraphBuilder
192193
App.Fossa.Analyze.GraphMangler
@@ -567,6 +568,7 @@ test-suite unit-tests
567568
AlpineLinux.ParserSpec
568569
Android.UtilSpec
569570
App.DocsSpec
571+
App.Fossa.Analyze.ForkAliasSpec
570572
App.Fossa.Analyze.UploadSpec
571573
App.Fossa.AnalyzeSpec
572574
App.Fossa.API.BuildLinkSpec
@@ -723,6 +725,7 @@ test-suite unit-tests
723725
Scala.SbtDependencyTreeParsingSpec
724726
Scala.SbtDependencyTreeSpec
725727
Sqlite.SqliteSpec
728+
Srclib.TypesSpec
726729
Swift.PackageResolvedSpec
727730
Swift.PackageSwiftSpec
728731
Swift.Xcode.PbxprojParserSpec

src/App/Fossa/Analyze.hs

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ module App.Fossa.Analyze (
1010
-- * Helpers
1111
toProjectResult,
1212
applyFiltersToProject,
13+
14+
-- * Fork alias translation (for testing)
15+
16+
-- Re-exported from App.Fossa.Analyze.ForkAlias
17+
translateDependency,
18+
translateDependencyGraph,
19+
translateLocatorWithForkAliases,
20+
mkForkAliasMap,
1321
) where
1422

1523
import App.Docs (userGuideUrl)
@@ -22,7 +30,15 @@ import App.Fossa.Analyze.Filter (
2230
CountedResult (..),
2331
checkForEmptyUpload,
2432
)
25-
import App.Fossa.Analyze.GraphMangler (graphingToGraph)
33+
import App.Fossa.Analyze.ForkAlias (
34+
buildProject,
35+
collectForkAliasLabels,
36+
mergeForkAliasLabels,
37+
mkForkAliasMap,
38+
translateDependency,
39+
translateDependencyGraph,
40+
translateLocatorWithForkAliases,
41+
)
2642
import App.Fossa.Analyze.Project (ProjectResult (..), mkResult)
2743
import App.Fossa.Analyze.ScanSummary (renderScanSummary)
2844
import App.Fossa.Analyze.Types (
@@ -32,7 +48,7 @@ import App.Fossa.Analyze.Types (
3248
DiscoveredProjectIdentifier (..),
3349
DiscoveredProjectScan (..),
3450
)
35-
import App.Fossa.Analyze.Upload (ScanUnits (SourceUnitOnly), mergeSourceAndLicenseUnits, uploadSuccessfulAnalysis)
51+
import App.Fossa.Analyze.Upload (ScanUnits (..), mergeSourceAndLicenseUnits, uploadSuccessfulAnalysis)
3652
import App.Fossa.BinaryDeps (analyzeBinaryDeps)
3753
import App.Fossa.Config.Analyze (
3854
AnalysisTacticTypes (..),
@@ -55,7 +71,11 @@ import App.Fossa.Ficus.Types (FicusAnalysisResults (vendoredDependencyScanResult
5571
import App.Fossa.FirstPartyScan (runFirstPartyScan)
5672
import App.Fossa.Lernie.Analyze (analyzeWithLernie)
5773
import App.Fossa.Lernie.Types (LernieResults (..))
58-
import App.Fossa.ManualDeps (analyzeFossaDepsFile)
74+
import App.Fossa.ManualDeps (
75+
ForkAlias (..),
76+
ManualDepsResult (..),
77+
analyzeFossaDepsFile,
78+
)
5979
import App.Fossa.PathDependency (enrichPathDependencies, enrichPathDependencies', withPathDependencyNudge)
6080
import App.Fossa.PreflightChecks (PreflightCommandChecks (AnalyzeChecks), preflightChecks)
6181
import App.Fossa.Reachability.Upload (analyzeForReachability, onlyFoundUnits)
@@ -104,6 +124,7 @@ import Data.Flag (Flag, fromFlag)
104124
import Data.Foldable (traverse_)
105125
import Data.Functor (($>))
106126
import Data.List.NonEmpty qualified as NE
127+
import Data.Map qualified as Map
107128
import Data.Maybe (catMaybes, fromMaybe, isJust, mapMaybe, maybeToList)
108129
import Data.String.Conversion (decodeUtf8, toText)
109130
import Data.Text.Extra (showT)
@@ -138,7 +159,13 @@ import Prettyprinter.Render.Terminal (
138159
color,
139160
)
140161
import Srclib.Converter qualified as Srclib
141-
import Srclib.Types (LicenseSourceUnit (..), Locator, SourceUnit, sourceUnitToFullSourceUnit)
162+
import Srclib.Types (
163+
LicenseSourceUnit (..),
164+
Locator (..),
165+
SourceUnit (..),
166+
sourceUnitToFullSourceUnit,
167+
translateSourceUnitLocators,
168+
)
142169
import System.FilePath ((</>))
143170
import Types (DiscoveredProject (..), FoundTargets)
144171

@@ -305,13 +332,15 @@ analyze cfg = Diag.context "fossa-analyze" $ do
305332
enableSnippetScan = Config.xSnippetScan cfg
306333
enableVendetta = Config.xVendetta cfg
307334

308-
manualSrcUnits <-
335+
manualDepsResult <-
309336
Diag.errorBoundaryIO . diagToDebug $
310337
if filterIsVSIOnly filters
311338
then do
312339
logInfo "Running in VSI only mode, skipping manual source units"
313-
pure Nothing
340+
pure $ ManualDepsResult Nothing []
314341
else Diag.context "fossa-deps" . runStickyLogger SevInfo $ analyzeFossaDepsFile basedir customFossaDepsFile maybeApiOpts vendoredDepsOptions
342+
let forkAliases = maybe [] manualDepsResultForkAliases (resultToMaybe manualDepsResult)
343+
manualSrcUnits = fmap manualDepsResultSourceUnit manualDepsResult
315344

316345
orgInfo <-
317346
for
@@ -473,10 +502,20 @@ analyze cfg = Diag.context "fossa-analyze" $ do
473502
(Nothing, Just lernie) -> Just lernie
474503
(Just firstParty, Nothing) -> Just firstParty
475504
let keywordSearchResultsFound = (maybe False (not . null . lernieResultsKeywordSearches) lernieResults)
476-
let outputResult = buildResult includeAll additionalSourceUnits filteredProjects' licenseSourceUnits
505+
let forkAliasMap = mkForkAliasMap forkAliases
506+
-- Collect labels from fork aliases to merge into source units
507+
let forkAliasLabels = collectForkAliasLabels forkAliases
508+
509+
-- Convert projects to source units and translate fork aliases in them
510+
let scannedSourceUnits = map (Srclib.projectToSourceUnit (fromFlag IncludeAll includeAll)) filteredProjects'
511+
let translatedAdditionalSourceUnits = map (translateSourceUnitLocators (translateLocatorWithForkAliases forkAliasMap)) additionalSourceUnits
512+
let translatedScannedSourceUnits = map (translateSourceUnitLocators (translateLocatorWithForkAliases forkAliasMap)) scannedSourceUnits
513+
let allTranslatedSourceUnits = map (mergeForkAliasLabels forkAliasLabels) (translatedAdditionalSourceUnits ++ translatedScannedSourceUnits)
514+
515+
let outputResult = buildResult allTranslatedSourceUnits filteredProjects' licenseSourceUnits forkAliasMap
477516

478517
scanUnits <-
479-
case (keywordSearchResultsFound, checkForEmptyUpload includeAll projectScans filteredProjects' additionalSourceUnits licenseSourceUnits) of
518+
case (keywordSearchResultsFound, checkForEmptyUpload projectScans filteredProjects' allTranslatedSourceUnits licenseSourceUnits) of
480519
(False, NoneDiscovered) -> Diag.warn ErrNoProjectsDiscovered $> emptyScanUnits
481520
(True, NoneDiscovered) -> Diag.warn ErrOnlyKeywordSearchResultsFound $> emptyScanUnits
482521
(False, FilteredAll) -> Diag.warn ErrFilteredAllProjects $> emptyScanUnits
@@ -613,27 +652,17 @@ instance Diag.ToDiagnostic AnalyzeError where
613652
]
614653
Errata (Just "Only keyword search results found") [] (Just body)
615654

616-
buildResult :: Flag IncludeAll -> [SourceUnit] -> [ProjectResult] -> Maybe LicenseSourceUnit -> Aeson.Value
617-
buildResult includeAll srcUnits projects licenseSourceUnits =
655+
buildResult :: [SourceUnit] -> [ProjectResult] -> Maybe LicenseSourceUnit -> Map.Map Locator ForkAlias -> Aeson.Value
656+
buildResult srcUnits projects licenseSourceUnits forkAliasMap =
618657
Aeson.object
619-
[ "projects" .= map buildProject projects
658+
[ "projects" .= map (buildProject forkAliasMap) projects
620659
, "sourceUnits" .= mergedUnits
621660
]
622661
where
623662
mergedUnits = case licenseSourceUnits of
624-
Nothing -> map sourceUnitToFullSourceUnit finalSourceUnits
663+
Nothing -> map sourceUnitToFullSourceUnit srcUnits
625664
Just licenseUnits -> do
626-
NE.toList $ mergeSourceAndLicenseUnits finalSourceUnits licenseUnits
627-
finalSourceUnits = srcUnits ++ scannedUnits
628-
scannedUnits = map (Srclib.projectToSourceUnit (fromFlag IncludeAll includeAll)) projects
629-
630-
buildProject :: ProjectResult -> Aeson.Value
631-
buildProject project =
632-
Aeson.object
633-
[ "path" .= projectResultPath project
634-
, "type" .= projectResultType project
635-
, "graph" .= graphingToGraph (projectResultGraph project)
636-
]
665+
NE.toList $ mergeSourceAndLicenseUnits srcUnits licenseUnits
637666

638667
updateProgress :: Has StickyLogger sig m => Progress -> m ()
639668
updateProgress Progress{..} =

0 commit comments

Comments
 (0)