Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b996be5
add fork aliases to fossa-deps.yml
spatten Nov 19, 2025
588795f
get tests passing
spatten Nov 19, 2025
3125e61
return the fork aliases when parsing manual deps file
spatten Nov 20, 2025
fff0da3
pass forkaliases into buildResult
spatten Nov 20, 2025
13d508b
do the locator translation
spatten Nov 20, 2025
ac28b29
do more of the work in translateSourceUnitLocators
spatten Nov 20, 2025
837f0b5
clean up a comment
spatten Nov 20, 2025
fb92a5a
remove unused imports
spatten Nov 20, 2025
18899a2
rename fields to my-fork and base
spatten Nov 20, 2025
43622d9
update the test
spatten Nov 20, 2025
9986897
clean up how we extract fork aliases
spatten Nov 20, 2025
fd2ed41
add a test
spatten Nov 20, 2025
98c310b
clean up a comment
spatten Nov 20, 2025
f549c1b
use expectationFailure
spatten Nov 20, 2025
e47e46d
Merge branch 'master' into fork-aliasing
spatten Nov 21, 2025
d1faec6
add labels to fork aliases
spatten Nov 21, 2025
e75d1de
translate dependencies in the graph too
spatten Nov 21, 2025
ffe34d1
do it on the thing we upload too
spatten Nov 21, 2025
f7893c0
only calculate forkAliasMap once
spatten Nov 21, 2025
067a905
only do the translation once
spatten Nov 21, 2025
1319566
fix unit tests
spatten Nov 27, 2025
7ef9db0
fix a lint
spatten Nov 28, 2025
b4ee1ec
get rid of a comment
spatten Nov 28, 2025
d0df0a4
add fork-aliasing to schema
spatten Nov 28, 2025
393f7fb
add fork-aliases to fossa-deps output by fossa init
spatten Nov 28, 2025
4ceb831
document it in fossa-deps.md
spatten Nov 28, 2025
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
28 changes: 28 additions & 0 deletions docs/references/files/fossa-deps.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,25 @@ vendored-dependencies:

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

### `fork-aliases:`

Denotes mapping of fork dependencies to their base dependencies. When a dependency matches a fork alias (by fetcher and project, ignoring version), it is replaced with the base locator, preserving the original version. This is useful when you have forked a dependency and want it to be treated as the original dependency in FOSSA.

- `my-fork`: The locator for your fork of the dependency. Format: `<fetcher>+<project>` or `<fetcher>+<project>$<revision>` (e.g., `cargo+my-serde` or `cargo+my-serde$1.0.0`). (Required)
- `base`: The locator for the base/original dependency that your fork should be aliased to. Format: `<fetcher>+<project>` or `<fetcher>+<project>$<revision>` (e.g., `cargo+serde` or `cargo+serde$1.0.0`). (Required)
- `labels`: An optional list of labels to be added to the fork alias.

```yaml
fork-aliases:
- my-fork: cargo+my-serde
base: cargo+serde
labels:
- label: internal
scope: org
```

> Note: Fork aliases match dependencies by fetcher and project name, ignoring version. When a match is found, the dependency is replaced with the base locator while preserving the original version from the fork.

## Labels

Each kind of dependency referenced above can have a `labels` field, which is a list of labels to be added to the dependency.
Expand Down Expand Up @@ -140,6 +159,15 @@ vendored-dependencies:
scope: project
- label: internal-dependency
scope: revision

fork-aliases:
- my-fork: cargo+my-serde
base: cargo+serde
labels:
- label: internal
scope: org
- label: fork-approved
scope: revision
```

## Errors in the `fossa-deps` file
Expand Down
33 changes: 33 additions & 0 deletions docs/references/files/fossa-deps.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,32 @@
"version"
],
"additionalProperties": false
},
"fork-alias": {
"properties": {
"my-fork": {
"type": "string",
"description": "The locator for your fork of the dependency. Format: <fetcher>+<project> or <fetcher>+<project>$<revision> (e.g., 'cargo+my-serde' or 'cargo+my-serde$1.0.0').",
"minLength": 1
},
"base": {
"type": "string",
"description": "The locator for the base/original dependency that your fork should be aliased to. Format: <fetcher>+<project> or <fetcher>+<project>$<revision> (e.g., 'cargo+serde' or 'cargo+serde$1.0.0').",
"minLength": 1
},
"labels": {
"type": "array",
"description": "Optional labels to be applied to the fork alias.",
"items": {
"$ref": "#/$defs/label"
}
}
},
"required": [
"my-fork",
"base"
],
"additionalProperties": false
}
},
"type": "object",
Expand Down Expand Up @@ -364,6 +390,13 @@
"items": {
"$ref": "#/$defs/remote-dependency"
}
},
"fork-aliases": {
"type": "array",
"description": "Fork aliases to map your fork dependencies to their base dependencies. When a dependency matches a fork alias (by fetcher and project, ignoring version), it is replaced with the base locator, preserving the original version.",
"items": {
"$ref": "#/$defs/fork-alias"
}
}
},
"required": []
Expand Down
1 change: 1 addition & 0 deletions spectrometer.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ test-suite unit-tests
Scala.SbtDependencyTreeParsingSpec
Scala.SbtDependencyTreeSpec
Sqlite.SqliteSpec
Srclib.TypesSpec
Swift.PackageResolvedSpec
Swift.PackageSwiftSpec
Swift.Xcode.PbxprojParserSpec
Expand Down
77 changes: 60 additions & 17 deletions src/App/Fossa/Analyze.hs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import App.Fossa.Analyze.Types (
DiscoveredProjectIdentifier (..),
DiscoveredProjectScan (..),
)
import App.Fossa.Analyze.Upload (ScanUnits (SourceUnitOnly), mergeSourceAndLicenseUnits, uploadSuccessfulAnalysis)
import App.Fossa.Analyze.Upload (ScanUnits (..), mergeSourceAndLicenseUnits, uploadSuccessfulAnalysis)
import App.Fossa.BinaryDeps (analyzeBinaryDeps)
import App.Fossa.Config.Analyze (
AnalysisTacticTypes (..),
Expand All @@ -54,7 +54,7 @@ import App.Fossa.Ficus.Analyze (analyzeWithFicus)
import App.Fossa.FirstPartyScan (runFirstPartyScan)
import App.Fossa.Lernie.Analyze (analyzeWithLernie)
import App.Fossa.Lernie.Types (LernieResults (..))
import App.Fossa.ManualDeps (analyzeFossaDepsFile)
import App.Fossa.ManualDeps (ForkAlias (..), ManualDepsResult (..), analyzeFossaDepsFile)
import App.Fossa.PathDependency (enrichPathDependencies, enrichPathDependencies', withPathDependencyNudge)
import App.Fossa.PreflightChecks (PreflightCommandChecks (AnalyzeChecks), preflightChecks)
import App.Fossa.Reachability.Upload (analyzeForReachability, onlyFoundUnits)
Expand Down Expand Up @@ -103,10 +103,12 @@ import Data.Flag (Flag, fromFlag)
import Data.Foldable (traverse_)
import Data.Functor (($>))
import Data.List.NonEmpty qualified as NE
import Data.Map qualified as Map
import Data.Maybe (fromMaybe, isJust, mapMaybe)
import Data.String.Conversion (decodeUtf8, toText)
import Data.Text.Extra (showT)
import Data.Traversable (for)
import DepTypes (Dependency (..))
import Diag.Diagnostic as DI
import Diag.Result (Result (Success), resultToMaybe)
import Discovery.Archive qualified as Archive
Expand All @@ -124,6 +126,8 @@ import Effect.Logger (
import Effect.ReadFS (ReadFS)
import Errata (Errata (..))
import Fossa.API.Types (Organization (Organization, orgSnippetScanSourceCodeRetentionDays, orgSupportsReachability))
import Graphing (Graphing)
import Graphing qualified
import Path (Abs, Dir, Path, toFilePath)
import Path.IO (makeRelative)
import Prettyprinter (
Expand All @@ -136,8 +140,16 @@ import Prettyprinter.Render.Terminal (
Color (Cyan, Green, Yellow),
color,
)
import Srclib.Converter (fetcherToDepType, toLocator)
import Srclib.Converter qualified as Srclib
import Srclib.Types (LicenseSourceUnit (..), Locator, SourceUnit, sourceUnitToFullSourceUnit)
import Srclib.Types (
LicenseSourceUnit (..),
Locator (..),
SourceUnit (..),
sourceUnitToFullSourceUnit,
toProjectLocator,
translateSourceUnitLocators,
)
import System.FilePath ((</>))
import Types (DiscoveredProject (..), FoundTargets)

Expand Down Expand Up @@ -303,13 +315,15 @@ analyze cfg = Diag.context "fossa-analyze" $ do
withoutDefaultFilters = Config.withoutDefaultFilters cfg
enableSnippetScan = Config.xSnippetScan cfg

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

orgInfo <-
for
Expand Down Expand Up @@ -462,10 +476,18 @@ analyze cfg = Diag.context "fossa-analyze" $ do
(Nothing, Just lernie) -> Just lernie
(Just firstParty, Nothing) -> Just firstParty
let keywordSearchResultsFound = (maybe False (not . null . lernieResultsKeywordSearches) lernieResults)
let outputResult = buildResult includeAll additionalSourceUnits filteredProjects' licenseSourceUnits
let forkAliasMap = mkForkAliasMap forkAliases

-- Convert projects to source units and translate fork aliases in them
let scannedSourceUnits = map (Srclib.projectToSourceUnit (fromFlag IncludeAll includeAll)) filteredProjects'
let translatedAdditionalSourceUnits = map (translateSourceUnitLocators forkAliasMap) additionalSourceUnits
let translatedScannedSourceUnits = map (translateSourceUnitLocators forkAliasMap) scannedSourceUnits
let allTranslatedSourceUnits = translatedAdditionalSourceUnits ++ translatedScannedSourceUnits

let outputResult = buildResult allTranslatedSourceUnits filteredProjects' licenseSourceUnits forkAliasMap

scanUnits <-
case (keywordSearchResultsFound, checkForEmptyUpload includeAll projectScans filteredProjects' additionalSourceUnits licenseSourceUnits) of
case (keywordSearchResultsFound, checkForEmptyUpload projectScans filteredProjects' allTranslatedSourceUnits licenseSourceUnits) of
(False, NoneDiscovered) -> Diag.warn ErrNoProjectsDiscovered $> emptyScanUnits
(True, NoneDiscovered) -> Diag.warn ErrOnlyKeywordSearchResultsFound $> emptyScanUnits
(False, FilteredAll) -> Diag.warn ErrFilteredAllProjects $> emptyScanUnits
Expand Down Expand Up @@ -602,28 +624,49 @@ instance Diag.ToDiagnostic AnalyzeError where
]
Errata (Just "Only keyword search results found") [] (Just body)

buildResult :: Flag IncludeAll -> [SourceUnit] -> [ProjectResult] -> Maybe LicenseSourceUnit -> Aeson.Value
buildResult includeAll srcUnits projects licenseSourceUnits =
buildResult :: [SourceUnit] -> [ProjectResult] -> Maybe LicenseSourceUnit -> Map.Map Locator Locator -> Aeson.Value
buildResult srcUnits projects licenseSourceUnits forkAliasMap =
Aeson.object
[ "projects" .= map buildProject projects
[ "projects" .= map (buildProject forkAliasMap) projects
, "sourceUnits" .= mergedUnits
]
where
mergedUnits = case licenseSourceUnits of
Nothing -> map sourceUnitToFullSourceUnit finalSourceUnits
Nothing -> map sourceUnitToFullSourceUnit srcUnits
Just licenseUnits -> do
NE.toList $ mergeSourceAndLicenseUnits finalSourceUnits licenseUnits
finalSourceUnits = srcUnits ++ scannedUnits
scannedUnits = map (Srclib.projectToSourceUnit (fromFlag IncludeAll includeAll)) projects
NE.toList $ mergeSourceAndLicenseUnits srcUnits licenseUnits

-- | Create a fork alias map from a list of fork aliases.
mkForkAliasMap :: [ForkAlias] -> Map.Map Locator Locator
mkForkAliasMap = Map.fromList . map (\ForkAlias{..} -> (toProjectLocator forkAliasMyFork, forkAliasBase))

buildProject :: ProjectResult -> Aeson.Value
buildProject project =
buildProject :: Map.Map Locator Locator -> ProjectResult -> Aeson.Value
buildProject forkAliasMap project =
Aeson.object
[ "path" .= projectResultPath project
, "type" .= projectResultType project
, "graph" .= graphingToGraph (projectResultGraph project)
, "graph" .= graphingToGraph (translateDependencyGraph forkAliasMap (projectResultGraph project))
]

-- | Translate dependencies in a graph using fork aliases.
-- When a dependency matches a fork alias (by fetcher and project, ignoring version),
-- it is replaced with the base locator, preserving the original version.
translateDependencyGraph :: Map.Map Locator Locator -> Graphing Dependency -> Graphing Dependency
translateDependencyGraph forkAliasMap = Graphing.gmap (translateDependency forkAliasMap)

-- | Translate a single dependency using fork aliases.
translateDependency :: Map.Map Locator Locator -> Dependency -> Dependency
translateDependency forkAliasMap dep =
case Map.lookup (toProjectLocator (toLocator dep)) forkAliasMap of
Nothing -> dep
Just baseLocator ->
let baseDepType = fromMaybe (dependencyType dep) (fetcherToDepType (locatorFetcher baseLocator))
baseName = locatorProject baseLocator
in dep
{ dependencyType = baseDepType
, dependencyName = baseName
}

updateProgress :: Has StickyLogger sig m => Progress -> m ()
updateProgress Progress{..} =
logSticky'
Expand Down
21 changes: 8 additions & 13 deletions src/App/Fossa/Analyze/Filter.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ module App.Fossa.Analyze.Filter (
import App.Fossa.Analyze.Project (ProjectResult)
import App.Fossa.Analyze.Types (DiscoveredProjectScan)
import App.Fossa.Analyze.Upload (ScanUnits (..))
import App.Fossa.Config.Analyze (IncludeAll (..))
import Data.Flag (Flag, fromFlag)
import Srclib.Converter qualified as Srclib
import Srclib.Types (LicenseSourceUnit (licenseSourceUnitLicenseUnits), LicenseUnit (licenseUnitName), SourceUnit)

data CountedResult
Expand All @@ -20,21 +17,21 @@ data CountedResult
-- that the smaller list is the latter, and return that list. Starting with user-defined deps,
-- we also include a check for an additional source unit from fossa-deps.yml
-- and a check for any licenses found during the firstPartyScan
checkForEmptyUpload :: Flag IncludeAll -> [DiscoveredProjectScan] -> [ProjectResult] -> [SourceUnit] -> Maybe LicenseSourceUnit -> CountedResult
checkForEmptyUpload includeAll discovered filtered additionalUnits firstPartyScanResults = do
if null additionalUnits
checkForEmptyUpload :: [DiscoveredProjectScan] -> [ProjectResult] -> [SourceUnit] -> Maybe LicenseSourceUnit -> CountedResult
checkForEmptyUpload discovered filtered sourceUnits firstPartyScanResults = do
if null sourceUnits
then case (discoveredLen, filteredLen, licensesMaybeFound) of
(0, _, Nothing) -> NoneDiscovered
(_, 0, Nothing) -> FilteredAll
(0, 0, Just licenseSourceUnit) -> CountedScanUnits $ LicenseSourceUnitOnly licenseSourceUnit
(0, _, Just licenseSourceUnit) -> CountedScanUnits $ LicenseSourceUnitOnly licenseSourceUnit
(_, 0, Just licenseSourceUnit) -> CountedScanUnits $ LicenseSourceUnitOnly licenseSourceUnit
(_, _, Just licenseSourceUnit) -> CountedScanUnits $ SourceAndLicenseUnits discoveredUnits licenseSourceUnit
(_, _, Nothing) -> CountedScanUnits . SourceUnitOnly $ discoveredUnits
else -- If we have a additional source units, then there's always something to upload.
(_, _, Just licenseSourceUnit) -> CountedScanUnits $ SourceAndLicenseUnits sourceUnits licenseSourceUnit
(_, _, Nothing) -> CountedScanUnits . SourceUnitOnly $ sourceUnits
else -- If we have source units, then there's always something to upload.
case licensesMaybeFound of
Nothing -> CountedScanUnits . SourceUnitOnly $ additionalUnits ++ discoveredUnits
Just licenseSourceUnit -> CountedScanUnits $ SourceAndLicenseUnits (additionalUnits ++ discoveredUnits) licenseSourceUnit
Nothing -> CountedScanUnits . SourceUnitOnly $ sourceUnits
Just licenseSourceUnit -> CountedScanUnits $ SourceAndLicenseUnits sourceUnits licenseSourceUnit
where
discoveredLen = length discovered
filteredLen = length filtered
Expand All @@ -45,7 +42,5 @@ checkForEmptyUpload includeAll discovered filtered additionalUnits firstPartySca
then Just scanResults
else Nothing

-- The smaller list is the post-filter list, since filtering cannot add projects
discoveredUnits = map (Srclib.projectToSourceUnit (fromFlag IncludeAll includeAll)) filtered
isActualLicense :: LicenseUnit -> Bool
isActualLicense licenseUnit = licenseUnitName licenseUnit /= "No_license_found"
18 changes: 18 additions & 0 deletions src/App/Fossa/Init/fossa-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,21 @@
# metadata: # Metadata of the dependency (Optional)
# description: Django # Description of the dependency, this will be shown in FOSSA UI, and generated reports. (Optional)
# homepage: https://www.djangoproject.com # Homepage of the dependency, this will be shown in FOSSA UI, and generated reports. (Optional)
#
#
#
# # fork-aliases
# # --
# # Denotes mapping of fork dependencies to their base dependencies. When a dependency
# # matches a fork alias (by fetcher and project, ignoring version), it is replaced with
# # the base locator, preserving the original version. This is useful when you have forked
# # a dependency and want it to be treated as the original dependency in FOSSA.
# #
# # Learn more: https://github.com/fossas/fossa-cli/blob/master/docs/references/files/fossa-deps.md#fork-aliases
# #
# fork-aliases:
# - my-fork: cargo+my-serde # Locator for your fork. Format: <fetcher>+<project> or <fetcher>+<project>$<revision> (Required)
# base: cargo+serde # Locator for the base/original dependency. Format: <fetcher>+<project> or <fetcher>+<project>$<revision> (Required)
# labels: # Labels to be applied to the fork alias (Optional)
# - label: internal
# scope: org
Loading
Loading