From 66aa6389cd560900fc36f6f5644dd2f6936a7ec3 Mon Sep 17 00:00:00 2001 From: meghfossa <86321858+meghfossa@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:31:01 -0700 Subject: [PATCH] [Reachability] Scan summary and better docs (#1379) --- Changelog.md | 3 + docs/README.md | 1 + .../vuln_reachability.md} | 69 ++++++++--- integration-test/Reachability/UploadSpec.hs | 46 ++++---- src/App/Docs.hs | 4 + src/App/Fossa/Analyze.hs | 12 +- src/App/Fossa/Analyze/Debug.hs | 2 +- src/App/Fossa/Analyze/ScanSummary.hs | 49 +++++++- src/App/Fossa/Analyze/Types.hs | 17 +++ src/App/Fossa/Analyze/Upload.hs | 11 +- src/App/Fossa/Reachability/Types.hs | 8 ++ src/App/Fossa/Reachability/Upload.hs | 57 +++++---- test/Reachability/UploadSpec.hs | 110 ++++++++++-------- 13 files changed, 259 insertions(+), 130 deletions(-) rename docs/{contributing/reachability.md => features/vuln_reachability.md} (65%) diff --git a/Changelog.md b/Changelog.md index 084dc780e6..884d8d3c52 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,8 @@ # FOSSA CLI Changelog +## v3.9.4 +- Reachability: Includes reachability analysis in scan summary [#1379](https://github.com/fossas/fossa-cli/pull/1379) + ## v3.9.3 - Update error structure [#1364](https://github.com/fossas/fossa-cli/pull/1364) diff --git a/docs/README.md b/docs/README.md index 9086839f1c..aa1c599137 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,6 +59,7 @@ Concept guides explain the nuances behind how basic FOSSA primitives work. If yo - [Analyzing Specific Submodules](./walkthroughs/analysis-target-configuration.md#target-filtering-for-submodules) - [Dynamic Strategy Command Selection](./features/strategy-command-selection.md) - [`fossa analyze` Reference](./references/subcommands/analyze.md) +- [Vulnerable Reachability](./features/vuln_reachability.md) #### Manually Specifying Dependencies diff --git a/docs/contributing/reachability.md b/docs/features/vuln_reachability.md similarity index 65% rename from docs/contributing/reachability.md rename to docs/features/vuln_reachability.md index 6c09d2a243..58b472bef3 100644 --- a/docs/contributing/reachability.md +++ b/docs/features/vuln_reachability.md @@ -1,10 +1,12 @@ # Reachability ### What is Reachability? + Reachability Analysis is a security offering designed to enhance FOSSA's security analysis by providing context on vulnerable packages. It alleviates the constraints of traditional CVE assessments through the static analysis of application and dependency code, confirming the presence of vulnerable call paths. ### Limitations -- Reachability currently supports all Maven and Gradle projects dynamically analyzed by fossa-cli. + +- Reachability currently supports all Maven and Gradle projects dynamically analyzed by FOSSA CLI. - The target jar of the project must exist, prior to the analysis. If the jar artifact is not present, or FOSSA CLI fails to associate this jar with project, FOSSA CLI will not perform reachability analysis. - Reachability requires that `java` is present in PATH, and `java` version must be greater than `1.8` (jdk8+). @@ -73,6 +75,11 @@ cat fossa.debug.json | jq '.bundleReachabilityEndpoint' } ``` +FOSSA CLI uses [jar-callgraph-1.0.0.jar](../../scripts/jar-callgraph-1.0.0.jar) to infer call path edges. +FOSSA CLI uses `java -jar jar-callgraph-1.0.0.jar ./path/to/your/build.jar` command to record edges from +the your target jar. If you are running into issues with reachability, please confirm that you can execute +`java -jar jar-callgraph-1.0.0.jar ./path/to/your/build.jar` on your environment. + ## F.A.Q. @@ -132,3 +145,33 @@ You can inspect the data by running: # to endpoint for reachability analysis. ; cat fossa.debug.json | jq '.bundleReachabilityRaw' ``` + +2. How do I know if reachability is supported by my organization? + +FOSSA requires that `reachability` is enabled for your organization. If `reachability` is not enabled, +for your organization, you will see following message in output when `fossa analyze` is performed without +`--output or -o` mode. + +```text +Organization: (your orgId) does not support reachability! skipping reachability analysis upload! +``` + +To enable, `reachability` please contact your FOSSA account manager, or [FOSSA support](https://support.fossa.com). + + +3. How do I know if my project was analyzed for reachability by FOSSA CLI? + +FOSSA CLI will include scan summary for reachability analysis, as part of `fossa analyze` output. + +```text +Reachability analysis + ** maven project in "/Users/dev/code/example-projects/reachability/maven/vuln-function-used/": succeeded +``` + +| Summary | Meaning | +|----------------------------------|------------------------------------------------------------------------------| +| succeeded | Reachability analysis was successful | +| skipped (partial graph) | Project has partial dependency graph (e.g. missing transitive dependencies) | +| skipped (not supported) | Project is not supported for reachability analysis | +| skipped (no dependency analysis) | Project's dependencies were not analyzed, so reachability cannot be computed | + diff --git a/integration-test/Reachability/UploadSpec.hs b/integration-test/Reachability/UploadSpec.hs index e5a0afd857..2dae490b77 100644 --- a/integration-test/Reachability/UploadSpec.hs +++ b/integration-test/Reachability/UploadSpec.hs @@ -6,9 +6,9 @@ module Reachability.UploadSpec (spec) where import Analysis.FixtureUtils (FixtureEnvironment (..), TestC, testRunnerWithLogger, withResult) import App.Fossa.Analyze.Project (ProjectResult (..)) import App.Fossa.Analyze.Types ( - AnalysisScanResult (..), DiscoveredProjectIdentifier (..), DiscoveredProjectScan (..), + SourceUnitReachabilityAttempt (..), ) import App.Fossa.Reachability.Jar (callGraphFromJar) import App.Fossa.Reachability.Types ( @@ -20,6 +20,7 @@ import App.Fossa.Reachability.Types ( import App.Fossa.Reachability.Upload ( analyzeForReachability, callGraphOf, + onlyFoundUnits, ) import Data.ByteString.Lazy qualified as LB import Data.Foldable (for_) @@ -83,8 +84,9 @@ spec = describe "Reachability" $ do jarFile <- ( sampleMavenProjectJar) <$> runIO PIO.getCurrentDir it "should retrieve call graph" $ do - let expected = Just (mavenCompleteScanUnit projDir jarFile) - resp <- run java8 $ callGraphOf (mavenCompleteScan projDir) + let (dpi, dps) = mavenCompleteScan projDir + let expected = SourceUnitReachabilityFound dpi (Success [] (mavenCompleteScanUnit projDir jarFile)) + resp <- run java8 $ callGraphOf dps withResult resp $ \_ res -> res `shouldBe` expected describe "analyzeForReachability" $ do @@ -92,18 +94,10 @@ spec = describe "Reachability" $ do jarFile <- ( sampleMavenProjectJar) <$> runIO PIO.getCurrentDir it "should return analyzed reachability unit" $ do + let (_, dps) = mavenCompleteScan projDir let expected = [mavenCompleteScanUnit projDir jarFile] - let analysisResult = - AnalysisScanResult - [mavenCompleteScan projDir] - successNothing - successNothing - successNothing - successNothing - successNothing - - analyzed <- run java8 $ analyzeForReachability analysisResult - withResult analyzed $ \_ analyzed' -> analyzed' `shouldBe` expected + analyzed <- run java8 $ analyzeForReachability [dps] + withResult analyzed $ \_ analyzed' -> (onlyFoundUnits analyzed') `shouldBe` expected sampleMavenProjectDir :: Path Rel Dir sampleMavenProjectDir = $(mkRelDir "test/Reachability/testdata/maven-default/") @@ -111,7 +105,7 @@ sampleMavenProjectDir = $(mkRelDir "test/Reachability/testdata/maven-default/") sampleMavenProjectJar :: Path Rel File sampleMavenProjectJar = $(mkRelFile "test/Reachability/testdata/maven-default/target/project-1.0.0.jar") -mavenCompleteScan :: Path Abs Dir -> DiscoveredProjectScan +mavenCompleteScan :: Path Abs Dir -> (DiscoveredProjectIdentifier, DiscoveredProjectScan) mavenCompleteScan dir = mkDiscoveredProjectScan MavenProjectType dir Complete mavenCompleteScanUnit :: Path Abs Dir -> Path Abs File -> SourceUnitReachability @@ -123,23 +117,25 @@ mavenCompleteScanUnit projDir jarFile = (ContentRaw sampleJarParsedContent') ] -mkDiscoveredProjectScan :: DiscoveredProjectType -> Path Abs Dir -> GraphBreadth -> DiscoveredProjectScan +mkDiscoveredProjectScan :: DiscoveredProjectType -> Path Abs Dir -> GraphBreadth -> (DiscoveredProjectIdentifier, DiscoveredProjectScan) mkDiscoveredProjectScan projectType dir breadth = - Scanned - (DiscoveredProjectIdentifier dir projectType) - ( Success - [] - (ProjectResult projectType dir empty breadth mempty) - ) + ( dpi + , Scanned + dpi + ( Success + [] + (ProjectResult projectType dir empty breadth mempty) + ) + ) + where + dpi :: DiscoveredProjectIdentifier + dpi = DiscoveredProjectIdentifier dir projectType sampleJarFile :: IO (Path Abs File) sampleJarFile = do cwd <- PIO.getCurrentDir pure (cwd $(mkRelFile "test/Reachability/testdata/sample.jar")) -successNothing :: Result (Maybe a) -successNothing = Success [] Nothing - mkReachabilityUnit :: Path Abs Dir -> [ParsedJar] -> SourceUnitReachability mkReachabilityUnit dir jars = SourceUnitReachability diff --git a/src/App/Docs.hs b/src/App/Docs.hs index 4faf1f8942..7b5fc22016 100644 --- a/src/App/Docs.hs +++ b/src/App/Docs.hs @@ -10,6 +10,7 @@ module App.Docs ( pathDependencyDocsUrl, staticAndDynamicStrategies, apiKeyUrl, + vulnReachabilityProductDocsUrl, ) where import App.Version (versionOrBranch) @@ -53,3 +54,6 @@ staticAndDynamicStrategies = guidePathOf versionOrBranch "/docs/references/strat apiKeyUrl :: Text apiKeyUrl = guidePathOf versionOrBranch "/README.md#generating-an-api-key" + +vulnReachabilityProductDocsUrl :: Text +vulnReachabilityProductDocsUrl = "https://docs.fossa.com/docs/reachability" diff --git a/src/App/Fossa/Analyze.hs b/src/App/Fossa/Analyze.hs index 3129027514..5876c2c4fe 100644 --- a/src/App/Fossa/Analyze.hs +++ b/src/App/Fossa/Analyze.hs @@ -54,7 +54,7 @@ import App.Fossa.Lernie.Types (LernieResults (..)) import App.Fossa.ManualDeps (analyzeFossaDepsFile) import App.Fossa.PathDependency (enrichPathDependencies, enrichPathDependencies', withPathDependencyNudge) import App.Fossa.PreflightChecks (preflightChecks) -import App.Fossa.Reachability.Upload (analyzeForReachability) +import App.Fossa.Reachability.Upload (analyzeForReachability, onlyFoundUnits) import App.Fossa.Subcommand (SubCommand) import App.Fossa.VSI.DynLinked (analyzeDynamicLinkedDeps) import App.Fossa.VSI.IAT.AssertRevisionBinaries (assertRevisionBinaries) @@ -104,7 +104,7 @@ import Data.Maybe (fromMaybe, mapMaybe) import Data.String.Conversion (decodeUtf8, toText) import Data.Text.Extra (showT) import Diag.Diagnostic as DI -import Diag.Result (Result (..), resultToMaybe) +import Diag.Result (resultToMaybe) import Discovery.Archive qualified as Archive import Discovery.Filters (AllFilters, MavenScopeFilters, applyFilters, filterIsVSIOnly, ignoredPaths, isDefaultNonProductionPath) import Discovery.Projects (withDiscoveredProjects) @@ -400,12 +400,10 @@ analyze cfg = Diag.context "fossa-analyze" $ do (False, _) -> traverse (withPathDependencyNudge includeAll) filteredProjects logDebug $ "Filtered projects with path dependencies: " <> pretty (show filteredProjects') - let analysisResult = AnalysisScanResult projectScans vsiResults binarySearchResults manualSrcUnits dynamicLinkedResults maybeLernieResults - reachabilityUnitsResult <- Diag.errorBoundaryIO . diagToDebug $ analyzeForReachability analysisResult - reachabilityUnits <- case reachabilityUnitsResult of - Diag.Result.Failure _ _ -> pure [] - Diag.Result.Success _ units -> pure units + reachabilityUnitsResult <- analyzeForReachability projectScans + let reachabilityUnits = onlyFoundUnits reachabilityUnitsResult + let analysisResult = AnalysisScanResult projectScans vsiResults binarySearchResults manualSrcUnits dynamicLinkedResults maybeLernieResults reachabilityUnitsResult renderScanSummary (severity cfg) maybeEndpointAppVersion analysisResult cfg -- Need to check if vendored is empty as well, even if its a boolean that vendoredDeps exist diff --git a/src/App/Fossa/Analyze/Debug.hs b/src/App/Fossa/Analyze/Debug.hs index b01f746a94..389f85a97a 100644 --- a/src/App/Fossa/Analyze/Debug.hs +++ b/src/App/Fossa/Analyze/Debug.hs @@ -25,7 +25,7 @@ module App.Fossa.Analyze.Debug ( where import App.Fossa.EmbeddedBinary (themisVersion) -import App.Fossa.Reachability.Upload (reachabilityEndpointJson, reachabilityRawJson) +import App.Fossa.Reachability.Types (reachabilityEndpointJson, reachabilityRawJson) import App.Version (fullVersionDescription) import Control.Applicative (asum) import Control.Carrier.Debug ( diff --git a/src/App/Fossa/Analyze/ScanSummary.hs b/src/App/Fossa/Analyze/ScanSummary.hs index 8dc7dfb109..73f274da60 100644 --- a/src/App/Fossa/Analyze/ScanSummary.hs +++ b/src/App/Fossa/Analyze/ScanSummary.hs @@ -15,10 +15,12 @@ import App.Fossa.Analyze.Types ( AnalysisScanResult (AnalysisScanResult), DiscoveredProjectIdentifier (dpiProjectPath, dpiProjectType), DiscoveredProjectScan (..), + SourceUnitReachabilityAttempt (..), ) import App.Fossa.Config.Analyze (AnalysisTacticTypes (StaticOnly)) import App.Fossa.Config.Analyze qualified as Config import App.Fossa.Lernie.Types (LernieMatch (..), LernieMatchData (..), LernieResults (..)) +import App.Fossa.Reachability.Types (SourceUnitReachability (..)) import App.Version (fullVersionDescription) import Control.Carrier.Lift import Control.Effect.Diagnostics qualified as Diag (Diagnostics) @@ -171,7 +173,7 @@ renderScanSummary severity maybeEndpointVersion analysisResults cfg = do logInfo "------------" summarize :: Config.AnalyzeConfig -> Text -> AnalysisScanResult -> Maybe ([Doc AnsiStyle]) -summarize cfg endpointVersion (AnalysisScanResult dps vsi binary manualDeps dynamicLinkingDeps lernie) = +summarize cfg endpointVersion (AnalysisScanResult dps vsi binary manualDeps dynamicLinkingDeps lernie reachabilityAttempts) = if (numProjects totalScanCount <= 0) then Nothing else @@ -195,6 +197,7 @@ summarize cfg endpointVersion (AnalysisScanResult dps vsi binary manualDeps dyna <> summarizeSrcUnit "fossa-deps file analysis" (Just getManualVendorDepsIdentifier) manualDeps <> summarizeSrcUnit "Keyword Search" (Just getLernieIdentifier) (lernieResultsKeywordSearches <$$> lernie) <> summarizeSrcUnit "Custom-License Search" (Just getLernieIdentifier) (lernieResultsCustomLicenses <$$> lernie) + <> summarizeReachability "Reachability analysis" reachabilityAttempts <> [""] where vsiResults = summarizeSrcUnit "vsi analysis" (Just (join . map vsiSourceUnits)) vsi @@ -309,11 +312,31 @@ summarizeProjectScan (Scanned _ (Success wg pr)) = successColorCoded wg $ render summarizeProjectScan (SkippedDueToProvidedFilter dpi) = renderDiscoveredProjectIdentifier dpi <> skippedDueFilter summarizeProjectScan (SkippedDueToDefaultProductionFilter dpi) = renderDiscoveredProjectIdentifier dpi <> skippedDueNonProductionPathFiltering +summarizeReachability :: + Doc AnsiStyle -> + [SourceUnitReachabilityAttempt] -> + [Doc AnsiStyle] +summarizeReachability analysisHeader unitAttempts = + [analysisHeader] <> itemize (" *" <> listSymbol) summarizeReachabilityAttempt unitAttempts + +summarizeReachabilityAttempt :: SourceUnitReachabilityAttempt -> Doc AnsiStyle +summarizeReachabilityAttempt (SourceUnitReachabilityFound _ (Success wg unit)) = successColorCoded wg $ renderReachabilityResult unit <> renderSucceeded wg +summarizeReachabilityAttempt (SourceUnitReachabilityFound dpi (Failure _ _)) = failColorCoded $ renderDiscoveredProjectIdentifier dpi <> renderFailed +summarizeReachabilityAttempt (SourceUnitReachabilitySkippedPartialGraph dpi) = renderDiscoveredProjectIdentifier dpi <> skippedReachabilityDueToPartialGraph +summarizeReachabilityAttempt (SourceUnitReachabilitySkippedNotSupported dpi) = renderDiscoveredProjectIdentifier dpi <> skippedReachabilityDueToNotSupported +summarizeReachabilityAttempt (SourceUnitReachabilitySkippedMissingDependencyAnalysis dpi) = renderDiscoveredProjectIdentifier dpi <> skippedReachabilityDueToMissingAnalysis + ---------- Rendering Helpers logInfoVsep :: (Has Logger sig m) => [Doc AnsiStyle] -> m () logInfoVsep = traverse_ logInfo +renderReachabilityResult :: SourceUnitReachability -> Doc AnsiStyle +renderReachabilityResult unit = annotate bold projectTypeDoc <> pathDoc + where + pathDoc = " project in " <> viaShow (srcUnitManifest unit) + projectTypeDoc = pretty $ srcUnitType unit + renderDiscoveredProjectIdentifier :: DiscoveredProjectIdentifier -> Doc AnsiStyle renderDiscoveredProjectIdentifier dpi = renderProjectPathAndType (dpiProjectType dpi) (dpiProjectPath dpi) @@ -341,6 +364,15 @@ skippedDueFilter = ": skipped (exclusion filters)" skippedDueNonProductionPathFiltering :: Doc AnsiStyle skippedDueNonProductionPathFiltering = ": skipped (non-production path filtering)" +skippedReachabilityDueToPartialGraph :: Doc AnsiStyle +skippedReachabilityDueToPartialGraph = ": skipped (partial graph)" + +skippedReachabilityDueToNotSupported :: Doc AnsiStyle +skippedReachabilityDueToNotSupported = ": skipped (not supported)" + +skippedReachabilityDueToMissingAnalysis :: Doc AnsiStyle +skippedReachabilityDueToMissingAnalysis = ": skipped (no dependency analysis)" + renderSucceeded :: [EmittedWarn] -> Doc AnsiStyle renderSucceeded ew = if countWarnings ew == 0 @@ -369,8 +401,8 @@ countWarnings ws = isIgnoredErrGroup IgnoredErrGroup{} = True isIgnoredErrGroup _ = False -dumpResultLogsToTempFile :: (Has (Lift IO) sig m) => Config.AnalyzeConfig -> Data.Text.Text -> AnalysisScanResult -> m (Path Abs File) -dumpResultLogsToTempFile cfg endpointVersion (AnalysisScanResult projects vsi binary manualDeps dynamicLinkingDeps lernieResults) = do +dumpResultLogsToTempFile :: (Has (Lift IO) sig m) => Config.AnalyzeConfig -> Text -> AnalysisScanResult -> m (Path Abs File) +dumpResultLogsToTempFile cfg endpointVersion (AnalysisScanResult projects vsi binary manualDeps dynamicLinkingDeps lernieResults reachabilityAttempts) = do let doc = stripAnsiEscapeCodes . renderStrict @@ -386,13 +418,14 @@ dumpResultLogsToTempFile cfg endpointVersion (AnalysisScanResult projects vsi bi , renderSourceUnit "fossa-deps analysis" manualDeps , renderSourceUnit "Custom-license scan & Keyword Search" lernieResults ] + ++ renderReachability tmpDir <- sendIO getTempDir sendIO $ TIO.writeFile (fromAbsFile $ tmpDir scanSummaryFileName) doc pure (tmpDir scanSummaryFileName) where scanSummary :: [Doc AnsiStyle] - scanSummary = maybeToList (vsep <$> summarize cfg endpointVersion (AnalysisScanResult projects vsi binary manualDeps dynamicLinkingDeps lernieResults)) + scanSummary = maybeToList (vsep <$> summarize cfg endpointVersion (AnalysisScanResult projects vsi binary manualDeps dynamicLinkingDeps lernieResults reachabilityAttempts)) renderSourceUnit :: Doc AnsiStyle -> Result (Maybe a) -> Maybe (Doc AnsiStyle) renderSourceUnit header (Failure ws eg) = Just $ renderFailure ws eg $ vsep $ summarizeSrcUnit header Nothing (Failure ws eg) @@ -404,5 +437,13 @@ dumpResultLogsToTempFile cfg endpointVersion (AnalysisScanResult projects vsi bi renderDiscoveredProjectScanResult (Scanned dpi (Success ws res)) = renderSuccess ws $ summarizeProjectScan (Scanned dpi (Success ws res)) renderDiscoveredProjectScanResult _ = Nothing + renderReachability :: [Doc AnsiStyle] + renderReachability = ["Reachability"] <> (mapMaybe renderReachabilityDetailed reachabilityAttempts) + + renderReachabilityDetailed :: SourceUnitReachabilityAttempt -> Maybe (Doc AnsiStyle) + renderReachabilityDetailed (SourceUnitReachabilityFound _ (Success wg unit)) = renderSuccess wg $ renderReachabilityResult unit <> renderSucceeded wg + renderReachabilityDetailed (SourceUnitReachabilityFound dpi (Failure ws eg)) = Just $ renderFailure ws eg $ renderDiscoveredProjectIdentifier dpi <> renderFailed + renderReachabilityDetailed res = Just $ summarizeReachabilityAttempt res + scanSummaryFileName :: Path Rel File scanSummaryFileName = $(mkRelFile "fossa-analyze-scan-summary.txt") diff --git a/src/App/Fossa/Analyze/Types.hs b/src/App/Fossa/Analyze/Types.hs index 2e8d706af2..c7e79429df 100644 --- a/src/App/Fossa/Analyze/Types.hs +++ b/src/App/Fossa/Analyze/Types.hs @@ -7,11 +7,13 @@ module App.Fossa.Analyze.Types ( AnalyzeExperimentalPreferences (..), DiscoveredProjectScan (..), DiscoveredProjectIdentifier (..), + SourceUnitReachabilityAttempt (..), ) where import App.Fossa.Analyze.Project (ProjectResult) import App.Fossa.Config.Analyze (ExperimentalAnalyzeConfig) import App.Fossa.Lernie.Types (LernieResults) +import App.Fossa.Reachability.Types (SourceUnitReachability (..)) import Control.Effect.Debug (Debug) import Control.Effect.Diagnostics (Diagnostics, Has) import Control.Effect.Lift (Lift) @@ -78,8 +80,23 @@ data AnalysisScanResult = AnalysisScanResult , fossaDepsScanResult :: Result (Maybe SourceUnit) , dynamicLinkingResult :: Result (Maybe SourceUnit) , lernieResult :: Result (Maybe LernieResults) + , reachabilityResult :: [SourceUnitReachabilityAttempt] } +data SourceUnitReachabilityAttempt + = SourceUnitReachabilityFound DiscoveredProjectIdentifier (Result SourceUnitReachability) + | SourceUnitReachabilitySkippedPartialGraph DiscoveredProjectIdentifier + | SourceUnitReachabilitySkippedNotSupported DiscoveredProjectIdentifier + | SourceUnitReachabilitySkippedMissingDependencyAnalysis DiscoveredProjectIdentifier + deriving (Show) + +instance Eq SourceUnitReachabilityAttempt where + (SourceUnitReachabilityFound a (Success _ resA)) == (SourceUnitReachabilityFound b (Success _ resB)) = (a == b) && (resA == resB) + (SourceUnitReachabilitySkippedPartialGraph a) == (SourceUnitReachabilitySkippedPartialGraph b) = compare a b == EQ + (SourceUnitReachabilitySkippedNotSupported a) == (SourceUnitReachabilitySkippedNotSupported b) = compare a b == EQ + (SourceUnitReachabilitySkippedMissingDependencyAnalysis a) == (SourceUnitReachabilitySkippedMissingDependencyAnalysis b) = compare a b == EQ + _ == _ = False + data DiscoveredProjectScan = SkippedDueToProvidedFilter DiscoveredProjectIdentifier | SkippedDueToDefaultProductionFilter DiscoveredProjectIdentifier diff --git a/src/App/Fossa/Analyze/Upload.hs b/src/App/Fossa/Analyze/Upload.hs index eb8d4056af..e55c3c4f7c 100644 --- a/src/App/Fossa/Analyze/Upload.hs +++ b/src/App/Fossa/Analyze/Upload.hs @@ -6,6 +6,7 @@ module App.Fossa.Analyze.Upload ( ScanUnits (..), ) where +import App.Docs (vulnReachabilityProductDocsUrl) import App.Fossa.API.BuildLink (getFossaBuildUrl) import App.Fossa.Config.Analyze (JsonOutput (JsonOutput)) import App.Fossa.Reachability.Types (SourceUnitReachability) @@ -38,7 +39,7 @@ import Control.Effect.FossaApiClient ( ) import Control.Effect.Git (Git, fetchGitContributors) import Control.Effect.Lift (Lift) -import Control.Monad (unless, when) +import Control.Monad (when) import Data.Aeson ((.=)) import Data.Aeson qualified as Aeson import Data.Flag (Flag, fromFlag) @@ -53,7 +54,6 @@ import Effect.Logger ( Logger, Pretty (pretty), Severity (SevInfo), - logDebug, logError, logInfo, logStdout, @@ -111,10 +111,9 @@ uploadSuccessfulAnalysis (BaseDir basedir) metadata jsonOutput revision scanUnit if (orgSupportsReachability org) then void $ upload revision metadata reachabilityUnits - else - unless (null reachabilityUnits) $ - logDebug . pretty $ - "Organization: (" <> show (organizationId org) <> ") does not support reachability! skipping reachability analysis upload!" + else do + logInfo . pretty $ "Organization: (" <> show (organizationId org) <> ") does not support reachability. Skipping reachability analysis upload." + logInfo . pretty $ "For reachability, refer to: " <> vulnReachabilityProductDocsUrl logInfo "" logInfo ("Using project name: `" <> pretty (projectName revision) <> "`") diff --git a/src/App/Fossa/Reachability/Types.hs b/src/App/Fossa/Reachability/Types.hs index 1318e83edb..f3bbf14439 100644 --- a/src/App/Fossa/Reachability/Types.hs +++ b/src/App/Fossa/Reachability/Types.hs @@ -3,6 +3,8 @@ module App.Fossa.Reachability.Types ( SourceUnitReachability (..), ParsedJar (..), ContentRef (..), + reachabilityRawJson, + reachabilityEndpointJson, ) where import Data.Aeson (ToJSON (..), Value, object, (.=)) @@ -70,3 +72,9 @@ data ParsedJar = ParsedJar deriving (Eq, Ord, Show, Generic) instance ToJSON ParsedJar + +reachabilityRawJson :: Text +reachabilityRawJson = "reachability.raw.json" + +reachabilityEndpointJson :: Text +reachabilityEndpointJson = "reachability.endpoint.json" diff --git a/src/App/Fossa/Reachability/Upload.hs b/src/App/Fossa/Reachability/Upload.hs index fea3b0f760..69aa91ad70 100644 --- a/src/App/Fossa/Reachability/Upload.hs +++ b/src/App/Fossa/Reachability/Upload.hs @@ -1,16 +1,16 @@ module App.Fossa.Reachability.Upload ( analyzeForReachability, - reachabilityRawJson, - reachabilityEndpointJson, upload, dependenciesOf, callGraphOf, + onlyFoundUnits, ) where +import App.Fossa.Analyze.Debug (diagToDebug) import App.Fossa.Analyze.Project (ProjectResult (..)) import App.Fossa.Analyze.Types ( - AnalysisScanResult (..), DiscoveredProjectScan (..), + SourceUnitReachabilityAttempt (..), dpiProjectType, ) import App.Fossa.Reachability.Gradle (gradleJarCallGraph) @@ -20,16 +20,18 @@ import App.Fossa.Reachability.Types ( ContentRef (..), ParsedJar (..), SourceUnitReachability (..), + reachabilityEndpointJson, + reachabilityRawJson, ) import App.Types (ProjectMetadata, ProjectRevision) import Control.Algebra (Has) +import Control.Carrier.Diagnostics qualified as Diag import Control.Effect.Debug (Debug, debugMetadata) import Control.Effect.Diagnostics (Diagnostics, context) import Control.Effect.FossaApiClient (FossaApiClient, uploadBuildForReachability, uploadContentForReachability) import Control.Effect.Lift (Lift) import Data.List (nub) -import Data.Maybe (catMaybes) -import Data.Text (Text) +import Data.Maybe (mapMaybe) import Diag.Result (Result (..)) import Effect.Exec (Exec) import Effect.Logger (Logger, logDebug, logInfo, pretty) @@ -51,12 +53,11 @@ analyzeForReachability :: , Has (Lift IO) sig m , Has Debug sig m ) => - AnalysisScanResult -> - m [SourceUnitReachability] -analyzeForReachability analysisResult = context "reachability" $ do - let analyzerResult = analyzersScanResult analysisResult - units <- catMaybes <$> (traverse callGraphOf analyzerResult) - debugMetadata reachabilityRawJson units + [DiscoveredProjectScan] -> + m [SourceUnitReachabilityAttempt] +analyzeForReachability analyzerResult = context "reachability" $ do + units <- traverse callGraphOf analyzerResult + debugMetadata reachabilityRawJson (onlyFoundUnits units) pure units upload :: @@ -101,9 +102,10 @@ callGraphOf :: , Has Diagnostics sig m , Has Exec sig m , Has (Lift IO) sig m + , Has Debug sig m ) => DiscoveredProjectScan -> - m (Maybe SourceUnitReachability) + m SourceUnitReachabilityAttempt callGraphOf (Scanned dpi (Success _ projectResult)) = do let srcUnit = projectToSourceUnit False projectResult let dependencies = dependenciesOf srcUnit @@ -123,24 +125,30 @@ callGraphOf (Scanned dpi (Success _ projectResult)) = do -- used in the application to perform accurate analysis (Partial, _) -> do logInfo . pretty $ "FOSSA CLI does not support reachability analysis, with partial dependencies graph (skipping: " <> displayId <> ")" - pure Nothing + pure . SourceUnitReachabilitySkippedPartialGraph $ dpi (Complete, MavenProjectType) -> context "maven" $ do logDebug . pretty $ "Trying to infer build jars from maven project: " <> show (projectResultPath projectResult) - analysis <- mavenJarCallGraph (projectResultPath projectResult) - pure . Just $ unit{callGraphAnalysis = analysis} + analysis <- Diag.errorBoundaryIO . diagToDebug $ mavenJarCallGraph (projectResultPath projectResult) + case analysis of + Success wg r -> pure $ SourceUnitReachabilityFound dpi (Success wg $ unit{callGraphAnalysis = r}) + Failure wg eg -> pure $ SourceUnitReachabilityFound dpi (Failure wg eg) (Complete, GradleProjectType) -> context "gradle" $ do logDebug . pretty $ "Trying to infer build jars from gradle project: " <> show (projectResultPath projectResult) - analysis <- gradleJarCallGraph (projectResultPath projectResult) - pure . Just $ unit{callGraphAnalysis = analysis} - + analysis <- Diag.errorBoundaryIO . diagToDebug $ gradleJarCallGraph (projectResultPath projectResult) + case analysis of + Success wg r -> pure $ SourceUnitReachabilityFound dpi (Success wg $ unit{callGraphAnalysis = r}) + Failure wg eg -> pure $ SourceUnitReachabilityFound dpi (Failure wg eg) -- Exclude units for package manager/language we cannot support yet! _ -> do + -- Update docs: ./docs/features/vuln_reachability.md logInfo . pretty $ "FOSSA CLI does not support reachability analysis for: " <> displayId <> " yet. (skipping)" - pure Nothing + pure . SourceUnitReachabilitySkippedNotSupported $ dpi -- Not possible to perform reachability analysis for projects -- which were not scanned (skipped due to filter), as we do not -- complete dependency graph for them -callGraphOf _ = pure Nothing +callGraphOf (SkippedDueToProvidedFilter dpi) = pure . SourceUnitReachabilitySkippedMissingDependencyAnalysis $ dpi +callGraphOf (SkippedDueToDefaultProductionFilter dpi) = pure . SourceUnitReachabilitySkippedMissingDependencyAnalysis $ dpi +callGraphOf (Scanned dpi (Failure _ _)) = pure . SourceUnitReachabilitySkippedMissingDependencyAnalysis $ dpi -- | Unique locators from SourceUnit dependenciesOf :: SourceUnit -> [Locator] @@ -153,8 +161,9 @@ allLocators unit = buildImports unit ++ concatMap (\ud -> sourceDepLocator ud : sourceDepImports ud) (buildDependencies unit) -reachabilityRawJson :: Text -reachabilityRawJson = "reachability.raw.json" +onlyFoundUnits :: [SourceUnitReachabilityAttempt] -> [SourceUnitReachability] +onlyFoundUnits = mapMaybe toFoundUnits -reachabilityEndpointJson :: Text -reachabilityEndpointJson = "reachability.endpoint.json" +toFoundUnits :: SourceUnitReachabilityAttempt -> Maybe SourceUnitReachability +toFoundUnits (SourceUnitReachabilityFound _ (Success _ src)) = Just src +toFoundUnits _ = Nothing diff --git a/test/Reachability/UploadSpec.hs b/test/Reachability/UploadSpec.hs index 55ccc4aca1..c0753a95d3 100644 --- a/test/Reachability/UploadSpec.hs +++ b/test/Reachability/UploadSpec.hs @@ -4,7 +4,11 @@ module Reachability.UploadSpec (spec) where import App.Fossa.Analyze.Project (ProjectResult (..)) -import App.Fossa.Analyze.Types (AnalysisScanResult (..), DiscoveredProjectIdentifier (..), DiscoveredProjectScan (..)) +import App.Fossa.Analyze.Types ( + DiscoveredProjectIdentifier (..), + DiscoveredProjectScan (..), + SourceUnitReachabilityAttempt (..), + ) import App.Fossa.Reachability.Types ( CallGraphAnalysis (JarAnalysis), ContentRef (ContentRaw, ContentStoreKey), @@ -20,6 +24,7 @@ import App.Fossa.Reachability.Upload ( analyzeForReachability, callGraphOf, dependenciesOf, + onlyFoundUnits, upload, ) import Control.Algebra (Has) @@ -75,41 +80,43 @@ dependenciesOfSpec = describe "dependenciesOf" $ callGraphOfSpec :: Spec callGraphOfSpec = describe "callGraphOf" $ do - it' "should not return reachability unit if project was skipped" $ do + it' "should return SkippedMissingDependencyAnalysis if project was skipped" $ do dir <- ( sampleMavenProjectDir) <$> PIO.getCurrentDir - res <- callGraphOf (skippedProject dir) - res `shouldBe'` Nothing + let (dps, dpi) = skippedProject dir + res <- callGraphOf dps + res `shouldBe'` (SourceUnitReachabilitySkippedMissingDependencyAnalysis dpi) - it' "should not return reachability unit if project was skipped due to default filtering" $ do + it' "should return SkippedMissingDependencyAnalysis if project was skipped due to default filtering" $ do dir <- ( sampleMavenProjectDir) <$> PIO.getCurrentDir - res <- callGraphOf (skippedProjectByDefaultFilter dir) - res `shouldBe'` Nothing + let (dps, dpi) = skippedProjectByDefaultFilter dir + res <- callGraphOf dps + res `shouldBe'` (SourceUnitReachabilitySkippedMissingDependencyAnalysis dpi) - it' "should not return reachability unit if graph depth is partial" $ do + it' "should return SkippedPartialGraph if graph depth is partial" $ do dir <- ( sampleMavenProjectDir) <$> PIO.getCurrentDir - res <- callGraphOf (mavenPartialScan dir) - res `shouldBe'` Nothing + let (dps, dpi) = (mavenPartialScan dir) + res <- callGraphOf dps + res `shouldBe'` SourceUnitReachabilitySkippedPartialGraph dpi - it' "should not reachability unit for non-mvn project" $ do + it' "should return SkippedNotSupported for non-mvn or gradle project" $ do dir <- ( sampleMavenProjectDir) <$> PIO.getCurrentDir - res <- callGraphOf (poetryCompleteScan dir) - res `shouldBe'` Nothing + let (dps, dpi) = (poetryCompleteScan dir) + res <- callGraphOf dps + res `shouldBe'` (SourceUnitReachabilitySkippedNotSupported dpi) analyzeForReachabilitySpec :: Spec analyzeForReachabilitySpec = describe "analyzeForReachability" $ it' "should return analyzed reachability unit" $ do dir <- ( sampleMavenProjectDir) <$> PIO.getCurrentDir - let analysisResult = - mkAnalysisResult - [ skippedProject dir - , skippedProjectByDefaultFilter dir - , mavenPartialScan dir - , poetryCompleteScan dir - ] - - analyzed <- analyzeForReachability analysisResult - analyzed `shouldBe'` [] + analyzed <- + analyzeForReachability + [ fst $ skippedProject dir + , fst $ skippedProjectByDefaultFilter dir + , fst $ mavenPartialScan dir + , fst $ poetryCompleteScan dir + ] + (onlyFoundUnits analyzed) `shouldBe'` [] uploadSpec :: Spec uploadSpec = describe "dependenciesOf" $ do @@ -137,40 +144,43 @@ sampleMavenProjectDir = $(mkRelDir "test/Reachability/testdata/maven-default/") sampleJar :: Path Rel File sampleJar = $(mkRelFile "test/Reachability/testdata/maven-default/target/project-1.0.0.jar") -skippedProject :: Path Abs Dir -> DiscoveredProjectScan +skippedProject :: Path Abs Dir -> (DiscoveredProjectScan, DiscoveredProjectIdentifier) skippedProject dir = - SkippedDueToProvidedFilter - (DiscoveredProjectIdentifier dir MavenProjectType) - -skippedProjectByDefaultFilter :: Path Abs Dir -> DiscoveredProjectScan + ( SkippedDueToProvidedFilter dpi + , dpi + ) + where + dpi :: DiscoveredProjectIdentifier + dpi = DiscoveredProjectIdentifier dir MavenProjectType + +skippedProjectByDefaultFilter :: Path Abs Dir -> (DiscoveredProjectScan, DiscoveredProjectIdentifier) skippedProjectByDefaultFilter dir = - SkippedDueToDefaultProductionFilter - (DiscoveredProjectIdentifier dir MavenProjectType) - -mavenPartialScan :: Path Abs Dir -> DiscoveredProjectScan + ( SkippedDueToDefaultProductionFilter dpi + , dpi + ) + where + dpi :: DiscoveredProjectIdentifier + dpi = DiscoveredProjectIdentifier dir MavenProjectType + +mavenPartialScan :: Path Abs Dir -> (DiscoveredProjectScan, DiscoveredProjectIdentifier) mavenPartialScan dir = mkDiscoveredProjectScan MavenProjectType dir Partial -poetryCompleteScan :: Path Abs Dir -> DiscoveredProjectScan +poetryCompleteScan :: Path Abs Dir -> (DiscoveredProjectScan, DiscoveredProjectIdentifier) poetryCompleteScan dir = mkDiscoveredProjectScan PoetryProjectType dir Complete -mkDiscoveredProjectScan :: DiscoveredProjectType -> Path Abs Dir -> GraphBreadth -> DiscoveredProjectScan +mkDiscoveredProjectScan :: DiscoveredProjectType -> Path Abs Dir -> GraphBreadth -> (DiscoveredProjectScan, DiscoveredProjectIdentifier) mkDiscoveredProjectScan projectType dir breadth = - Scanned - (DiscoveredProjectIdentifier dir projectType) - ( Success - [] - (ProjectResult projectType dir empty breadth mempty) - ) - -mkAnalysisResult :: [DiscoveredProjectScan] -> AnalysisScanResult -mkAnalysisResult dps = - AnalysisScanResult - dps - (Success [] Nothing) - (Success [] Nothing) - (Success [] Nothing) - (Success [] Nothing) - (Success [] Nothing) + ( Scanned + dpi + ( Success + [] + (ProjectResult projectType dir empty breadth mempty) + ) + , dpi + ) + where + dpi :: DiscoveredProjectIdentifier + dpi = DiscoveredProjectIdentifier dir projectType mkParsedJarRaw :: Path Abs File -> ByteString -> ParsedJar mkParsedJarRaw file bs =