Skip to content

Commit 2f8706f

Browse files
committed
[ANE-2484] Call ficus
1 parent ebe802c commit 2f8706f

4 files changed

Lines changed: 379 additions & 8 deletions

File tree

spectrometer.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ library
246246
App.Fossa.DumpBinaries
247247
App.Fossa.EmbeddedBinary
248248
App.Fossa.Ficus.Analyze
249+
App.Fossa.Ficus.Types
249250
App.Fossa.FirstPartyScan
250251
App.Fossa.Init
251252
App.Fossa.Lernie.Analyze

src/App/Fossa/EmbeddedBinary.hs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
module App.Fossa.EmbeddedBinary (
66
BinaryPaths,
7+
Ficus,
78
Lernie,
89
ThemisIndex,
910
ThemisBins (..),
1011
toPath,
1112
withThemisAndIndex,
1213
withBerkeleyBinary,
14+
withFicusBinary,
1315
withLernieBinary,
1416
withMillhoneBinary,
1517
withCirceBinary,
@@ -57,6 +59,7 @@ data PackagedBinary
5759
= Themis
5860
| ThemisIndex
5961
| BerkeleyDB
62+
| Ficus
6063
| Lernie
6164
| Millhone
6265
| Circe
@@ -77,6 +80,8 @@ data ThemisIndex
7780

7881
data Lernie
7982

83+
data Ficus
84+
8085
data Circe
8186

8287
data ThemisBins = ThemisBins
@@ -116,6 +121,9 @@ withBerkeleyBinary = withEmbeddedBinary BerkeleyDB
116121
withLernieBinary :: (Has (Lift IO) sig m) => (BinaryPaths -> m c) -> m c
117122
withLernieBinary = withEmbeddedBinary Lernie
118123

124+
withFicusBinary :: (Has (Lift IO) sig m) => (BinaryPaths -> m c) -> m c
125+
withFicusBinary = withEmbeddedBinary Ficus
126+
119127
withMillhoneBinary :: (Has (Lift IO) sig m) => (BinaryPaths -> m c) -> m c
120128
withMillhoneBinary = withEmbeddedBinary Millhone
121129

@@ -131,7 +139,6 @@ cleanupExtractedBinaries (BinaryPaths binPath _) = sendIO $ removeDirRecur binPa
131139
extractEmbeddedBinary :: (Has (Lift IO) sig m) => PackagedBinary -> m BinaryPaths
132140
extractEmbeddedBinary bin = do
133141
container <- sendIO extractDir
134-
-- Determine paths to which we should write the binaries
135142
let binPath = extractedPath bin
136143
-- Write the binary
137144
sendIO $ writeBinary (container </> binPath) bin
@@ -144,13 +151,15 @@ dumpEmbeddedBinary dir bin = writeBinary path bin
144151
path = dir </> extractedPath bin
145152

146153
writeBinary :: (Has (Lift IO) sig m) => Path Abs File -> PackagedBinary -> m ()
147-
writeBinary dest bin = sendIO . writeExecutable dest $ case bin of
148-
Themis -> embeddedBinaryThemis
149-
ThemisIndex -> embeddedBinaryThemisIndex
150-
BerkeleyDB -> embeddedBinaryBerkeleyDB
151-
Lernie -> embeddedBinaryLernie
152-
Millhone -> embeddedBinaryMillhone
153-
Circe -> embeddedBinaryCirce
154+
writeBinary dest bin = do
155+
sendIO . writeExecutable dest $ case bin of
156+
Themis -> embeddedBinaryThemis
157+
ThemisIndex -> embeddedBinaryThemisIndex
158+
BerkeleyDB -> embeddedBinaryBerkeleyDB
159+
Ficus -> embeddedBinaryFicus
160+
Lernie -> embeddedBinaryLernie
161+
Millhone -> embeddedBinaryMillhone
162+
Circe -> embeddedBinaryCirce
154163

155164
writeExecutable :: Path Abs File -> ByteString -> IO ()
156165
writeExecutable path content = do
@@ -163,6 +172,7 @@ extractedPath bin = case bin of
163172
Themis -> $(mkRelFile "themis-cli")
164173
ThemisIndex -> $(mkRelFile "index.gob.xz")
165174
BerkeleyDB -> $(mkRelFile "berkeleydb-plugin")
175+
Ficus -> $(mkRelFile "ficus")
166176
Lernie -> $(mkRelFile "lernie")
167177
Millhone -> $(mkRelFile "millhone")
168178
Circe -> $(mkRelFile "circe")
@@ -211,6 +221,9 @@ embeddedBinaryLernie = $(embedFileIfExists "vendor-bins/lernie")
211221
embeddedBinaryCirce :: ByteString
212222
embeddedBinaryCirce = $(embedFileIfExists "vendor-bins/circe")
213223

224+
embeddedBinaryFicus :: ByteString
225+
embeddedBinaryFicus = $(embedFileIfExists "vendor-bins/ficus")
226+
214227
-- To build this, run `make build` or `cargo build --release`.
215228
#ifdef mingw32_HOST_OS
216229
embeddedBinaryMillhone :: ByteString

src/App/Fossa/Ficus/Analyze.hs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
3+
module App.Fossa.Ficus.Analyze (
4+
analyzeWithFicus
5+
) where
6+
7+
import App.Fossa.EmbeddedBinary (BinaryPaths, toPath, withFicusBinary)
8+
import App.Fossa.Ficus.Types (FicusSnippetScanResults (..), FicusMessages (..), FicusMessage (..), FicusConfig (..), FicusDebug (..), FicusMessageData (..), FicusError (..), FicusFinding (..), FicusAnalysisFlag (..), FicusAllFlag (..), FicusPerStrategyFlag (..))
9+
import App.Types (FileUpload (..), ProjectRevision (..))
10+
import Control.Carrier.Debug (Debug)
11+
import Control.Carrier.Diagnostics (Diagnostics, fatal, warn)
12+
import Control.Carrier.FossaApiClient (runFossaApiClient)
13+
import Control.Carrier.Telemetry.Types
14+
import Control.Effect.FossaApiClient (FossaApiClient, getOrganization)
15+
import Control.Effect.Lift (Has, Lift)
16+
import Control.Effect.Telemetry (Telemetry, trackUsage)
17+
import Control.Monad (join, unless)
18+
import Data.Aeson (decode, Object, (.:))
19+
import Data.Aeson.Types (parseMaybe)
20+
import Data.ByteString.Lazy qualified as BL
21+
import Data.Foldable (fold, traverse_)
22+
import Data.HashMap.Internal.Strict (HashMap)
23+
import Data.HashMap.Strict qualified as H
24+
import Data.HashMap.Strict qualified as HashMap
25+
import Data.Hashable (Hashable)
26+
import Data.List (nub)
27+
import Data.List.NonEmpty qualified as NE
28+
import Data.Maybe (mapMaybe)
29+
import Data.String.Conversion (ToText (toText), decodeUtf8)
30+
import Data.Text (Text)
31+
import Data.Text qualified as Text
32+
import Data.Text.Encoding qualified as Text.Encoding
33+
import Effect.Exec (AllowErr (Never), Command (..), Exec, execCurrentDirStdinThrow, execThrow')
34+
import Effect.ReadFS (ReadFS)
35+
import Fossa.API.Types (ApiKey (..), ApiOpts (..), Organization (..), orgFileUpload)
36+
import Path (Abs, Dir, File, Path, toFilePath)
37+
import Prettyprinter (pretty)
38+
import Srclib.Types
39+
( LicenseScanType (..)
40+
, LicenseSourceUnit (..)
41+
, LicenseUnit (..)
42+
, LicenseUnitData (..)
43+
, LicenseUnitInfo (..)
44+
, LicenseUnitMatchData (..)
45+
, Locator (..)
46+
, renderLocator
47+
)
48+
import Effect.Logger (Logger, logDebug, logInfo, logError)
49+
import Text.URI (render)
50+
import Text.URI.Builder (PathComponent (..), TrailingSlash (..), setPath)
51+
import Types (GlobFilter (..), LicenseScanPathFilters (..), LicenseScanPathFilters)
52+
import Prelude hiding (unwords)
53+
-- | scan rootDir with Ficus, using the given GrepOptions. This is the main entry point to this module
54+
analyzeWithFicus ::
55+
( Has Diagnostics sig m
56+
, Has (Lift IO) sig m
57+
, Has Exec sig m
58+
, Has ReadFS sig m
59+
, Has Logger sig m
60+
) =>
61+
Path Abs Dir ->
62+
Maybe ApiOpts ->
63+
ProjectRevision ->
64+
Maybe LicenseScanPathFilters ->
65+
m (Maybe FicusSnippetScanResults)
66+
analyzeWithFicus rootDir apiOpts revision filters = do
67+
logInfo $ "Running Ficus analysis on " <> pretty (toFilePath rootDir)
68+
messages <- runFicus ficusConfig
69+
let ficusResults = ficusMessagesToFicusSnippetScanResults messages
70+
case ficusResults of
71+
Just results -> do
72+
logInfo $ "Ficus analysis completed successfully with analysis ID: " <> pretty (ficusSnippetScanResultsAnalysisId results)
73+
Nothing -> logError "Ficus analysis completed but no fingerprint findings were found"
74+
pure ficusResults
75+
where
76+
ficusConfig =
77+
FicusConfig
78+
{ ficusConfigRootDir = rootDir
79+
, ficusConfigExclude = maybe [] licenseScanPathFiltersExclude filters
80+
, ficusConfigEndpoint = apiOptsUri =<< apiOpts
81+
, ficusConfigSecret = apiOptsApiKey <$> apiOpts
82+
, ficusConfigRevision = revision
83+
, ficusConfigFlags = [All $ FicusAllFlag SkipHiddenFiles, All $ FicusAllFlag Gitignore]
84+
}
85+
86+
runFicus ::
87+
( Has Diagnostics sig m
88+
, Has (Lift IO) sig m
89+
, Has ReadFS sig m
90+
, Has Exec sig m
91+
, Has Logger sig m
92+
) =>
93+
FicusConfig ->
94+
m FicusMessages
95+
runFicus ficusConfig = do
96+
withFicusBinary $ \bin -> do
97+
cmd <- ficusCommand ficusConfig bin
98+
logDebug $ "Executing Ficus command: " <> pretty (show cmd)
99+
result <- execThrow' cmd
100+
let messages = parseFicusJson result
101+
102+
-- Log findings at debug level for visibility
103+
traverse_ (logDebug . pretty . displayFicusFinding) $ ficusMessageFindings messages
104+
traverse_ (logError . pretty . displayFicusError) $ ficusMessageErrors messages
105+
traverse_ (logDebug . pretty . displayFicusDebug) $ ficusMessageDebugs messages
106+
107+
pure messages
108+
where
109+
displayFicusDebug :: FicusDebug -> Text
110+
displayFicusDebug (FicusDebug FicusMessageData{..}) = "DEBUG " <> ficusMessageDataStrategy <> ": " <> ficusMessageDataPayload
111+
displayFicusError :: FicusError -> Text
112+
displayFicusError (FicusError FicusMessageData{..}) = "ERROR " <> ficusMessageDataStrategy <> ": " <> ficusMessageDataPayload
113+
displayFicusFinding :: FicusFinding -> Text
114+
displayFicusFinding (FicusFinding FicusMessageData{..}) = "FINDING " <> ficusMessageDataStrategy <> ": " <> ficusMessageDataPayload
115+
116+
117+
ficusMessagesToFicusSnippetScanResults :: FicusMessages -> Maybe FicusSnippetScanResults
118+
ficusMessagesToFicusSnippetScanResults messages =
119+
let isFingerprintStrategy :: FicusFinding -> Bool
120+
isFingerprintStrategy (FicusFinding (FicusMessageData strategy _)) =
121+
Text.toLower strategy == "fingerprint"
122+
123+
extractAnalysisId :: FicusFinding -> Maybe Int
124+
extractAnalysisId (FicusFinding (FicusMessageData _ payload)) =
125+
case decode (BL.fromStrict $ Text.Encoding.encodeUtf8 payload) :: Maybe Object of
126+
Just obj -> parseMaybe (.: "analysis_id") obj
127+
Nothing -> Nothing
128+
129+
matchingFinding = filter isFingerprintStrategy (ficusMessageFindings messages)
130+
analysisId = mapMaybe extractAnalysisId matchingFinding
131+
in case analysisId of
132+
(aid : _) -> Just $ FicusSnippetScanResults{ficusSnippetScanResultsAnalysisId = aid}
133+
[] -> Nothing
134+
135+
-- Run Ficus, passing config-based args as configuration.
136+
-- Caveat! This hard-codes some flags currently which may later need to be set on a strategy-by-strategy basis.
137+
ficusCommand :: Has Diagnostics sig m => FicusConfig -> BinaryPaths -> m Command
138+
ficusCommand ficusConfig bin = do
139+
endpoint <- case ficusConfigEndpoint ficusConfig of
140+
Just baseUri -> do
141+
proxyUri <- setPath [PathComponent "api", PathComponent "proxy"] (TrailingSlash False) baseUri
142+
pure $ render proxyUri
143+
Nothing -> pure ""
144+
pure $ Command
145+
{ cmdName = toText $ toPath bin
146+
, cmdArgs = configArgs endpoint
147+
, cmdAllowErr = Never
148+
}
149+
where
150+
configArgs endpoint = ["analyze", "--secret", secret, "--endpoint", endpoint, "--locator", locator, "--set", "all:skip-hidden-files", "--set", "all:gitignore"] ++ configExcludes ++ [targetDir]
151+
targetDir = toText $ toFilePath $ ficusConfigRootDir ficusConfig
152+
secret = maybe "" (toText . unApiKey) $ ficusConfigSecret ficusConfig
153+
locator = renderLocator $ Locator "custom" (projectName $ ficusConfigRevision ficusConfig) (Just $ projectRevision $ ficusConfigRevision ficusConfig)
154+
configExcludes = unGlobFilter <$> ficusConfigExclude ficusConfig
155+
156+
-- Parse Ficus's NDJson output by splitting on newlines (character 10) and
157+
-- then decoding each line
158+
parseFicusJson :: BL.ByteString -> FicusMessages
159+
parseFicusJson out =
160+
fold messages
161+
where
162+
messageLines = BL.splitWith (== 10) out
163+
-- Once Ficus supports file filtering we'll do this by passing the config file path into Ficus
164+
-- and Ficus will skip scanning the config file. But for now let's just filter post-scan
165+
parsedLines = mapMaybe decode messageLines
166+
messages = map singletonFicusMessage parsedLines
167+
168+
-- add a FicusMessage to the corresponding entry of an empty FicusMessages
169+
singletonFicusMessage :: FicusMessage -> FicusMessages
170+
singletonFicusMessage message = case message of
171+
FicusMessageFinding msg -> mempty{ficusMessageFindings = [msg]}
172+
FicusMessageDebug msg -> mempty{ficusMessageDebugs = [msg]}
173+
FicusMessageError msg -> mempty{ficusMessageErrors = [msg]}

0 commit comments

Comments
 (0)