Skip to content

Commit 47afb83

Browse files
csasarakjssblck
andauthored
[ANE-1809] - Partial distroless container support (#1448)
* Upgrade deps. Add fingerprint-lib as a dependency to millhone. * Initial subcommand for jar searching. * Can fingerprint jars in a container tarfile. * Better error handling. * Add config for millhone for where to output traces. * Output layer information. * Expose a way to get the stderr from a program when using the exec effect. * Run millhone jar analyze as part of container scanning. * Apply suggestions from code review (Jess) Co-authored-by: Jessica Black <[email protected]> * New module. * Address PR requests from Jess. * Update to latest lib-fingerprint. * Fix fingerprints name. * Parse Millhone's output properly. * Output the observation kind so the CLI can just pass observations through. * Create new output build type. Have the container analyzer include jar results. * A type for LayerPaths. * Add layer path to layers. * Use container jar output from millhone to augment Container scans. * Remove unneeded log statement. * Fix warning. * Hex encode millhone jar observation output. * Refactor analyze native to fetch the organization only once. * Implement fallback from sparkle to core. * Rename Makefile target. * Fix warning. * Apply suggestions from code review Co-authored-by: Jessica Black <[email protected]> * Rename analyze_jar to analyze_container. * Update comment, log message. * Update changelog. * Fix format. * Output better messaging if Jar analysis fails. * Update millhone cli command. * Blurb mentioning Jar analysis. * Remove bad underscore. * CLI-side tests. * Millhone test. * Test against abstract JSON. * fmt * WIP: Cli rough-cut but we need Core support to make this work. * Tests for JIC distroless container. * Fix millhone test. * Add a small npm project to the container. * Add docs. * Update changelog. * Make paths on windows work for test. * Update container scanning TOC. * Add informational messaging when no system info can be found. --------- Co-authored-by: Jessica Black <[email protected]>
1 parent 65046d8 commit 47afb83

File tree

15 files changed

+161
-52
lines changed

15 files changed

+161
-52
lines changed

Changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# FOSSA CLI Changelog
22

3+
## 3.9.28
4+
5+
- Container Scanning: Distroless containers will now return results for non-system dependencies. ([#1448](https://github.com/fossas/fossa-cli/pull/1448))
6+
37
## 3.9.27
48

59
- Tar: Move to the upstream Haskell tar library. FOSSA CLI should now work more reliably when unpacking containers for analysis. ([#1452](https://github.com/fossas/fossa-cli/pull/1452))

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ build-cargo:
2626
# make test ARGS="Node.PackageLockV3"
2727
test: test-cargo test-cabal
2828

29-
test-cabal:
29+
test-cabal: build-cargo
3030
ifdef ARGS
3131
cabal test unit-tests --test-show-details=streaming --test-option=--format=checks --test-option=--times --test-option=--color --test-option=--match --test-option="$(ARGS)"
3232
else

docs/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ Concept guides explain the nuances behind how basic FOSSA primitives work. If yo
9999
- [From Docker Engine](./references/subcommands/container/scanner.md#2-from-docker-engine)
100100
- [From Container Registries](./references/subcommands/container/scanner.md#3-from-registries)
101101
- [Supported Container Package Managers](./references/subcommands/container/scanner.md#supported-container-package-managers)
102-
- [Container Jar File Analysis](./references/subcommands/container/scanner.md#container-jar-analysis)
102+
- [Container Jar File Analysis](./references/subcommands/container/scanner.md#container-jar-analysis)
103+
- [Distroless Containers](./references/subcommands/container/scanner.md#distroless-containers)
103104
- [Viewing Detected Projects](./references/subcommands/container/scanner.md#view-detected-projects)
104105
- [Configuring Container Analysis Targets](./references/subcommands/container/scanner.md#utilize-analysis-target-configuration)
105106
- [Integrating Container Scanning in CI](./walkthroughs/container-scanning-generic-ci.md)

docs/references/subcommands/container/scanner.md

+7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- [3) From registries](#3-from-registries)
1010
- [Container image analysis](#container-image-analysis)
1111
- [Container Jar analysis](#container-jar-analysis)
12+
- [Distroless Containers](#distroless-containers)
13+
- [Supported Container Package Managers](#supported-container-package-managers)
1214
- [View detected projects](#view-detected-projects)
1315
- [Command output](#command-output)
1416
- [Utilize analysis target configuration](#utilize-analysis-target-configuration)
@@ -213,6 +215,11 @@ It will then report them to FOSSA which will try to match the Jar files to the p
213215
This process relies on there being a back-end that can perform that analysis.
214216
SaaS customers should have this functionality available but on-prem customers may need to contact FOSSA support to have it enabled.
215217

218+
### Distroless Containers
219+
220+
Container images where FOSSA cannot detect an operating system are supported but in a more limited way than images where FOSSA can.
221+
These container images will not support reporting system deps (APK, DPKG, and RPM) but can support the other forms of analyses listed in the table below.
222+
216223
### Supported Container Package Managers
217224
The following package managers are supported in container scanning:
218225

extlib/millhone/src/cmd/analyze_container.rs

+29-15
Original file line numberDiff line numberDiff line change
@@ -193,35 +193,49 @@ mod tests {
193193

194194
use super::*;
195195

196-
const MILLHONE_OUT: &str = r#"{
196+
const MILLHONE_OUT: &str = r#"
197+
{
197198
"discovered_jars": {
198-
"blobs/sha256/5c079c30beb013e4b2f7729b6bdce6fba57941d28f20db985333fc1dd969f018": [
199+
"blobs/sha256/61aed1a8baa251dee118b9ab203c1e420f0eda0a9b3f9322d67d235dd27a12ee": [
199200
{
200201
"kind": "v1.discover.binary.jar",
201-
"path": "inner_directory/commons-email2-jakarta-2.0.0-M1.jar",
202+
"path": "jackson-annotations-2.17.1.jar",
202203
"fingerprints": {
203-
"v1.mavencentral.jar": "6bzpyKql6Q+UxKQgm14pcP4wHGo=",
204-
"v1.raw.jar": "QA4SAurtJeo+lx1Vqve5uQYnvDKLFx1NgsSuoWKi8pw=",
205-
"sha_256": "MuEcK3nOFuySTTg4HOJi3vvTpI9bYspfMHa9AK2merQ=",
206-
"v1.class.jar": "2wRGbMGyGRwEXqNm53h1YK8OO879kvzDxazmJXiAcfI="
204+
"sha_256": "/MrYLhMXLA5DhNtxV3IZybhjHAgg9LGNqqVwFvtmHHY=",
205+
"v1.mavencentral.jar": "/KfvYZLJrQXQe8UNqZG/k3qErzo=",
206+
"v1.class.jar": "t2Btr6rNrvzghM5Nud2uldRGVjw0/n5rK9j0xooQQyk=",
207+
"v1.raw.jar": "wjGJk8cvY4tpKcUC5r8YuO15Wfv+rVuyWANBYCUIeDs="
207208
}
208209
}
209210
],
210-
"blobs/sha256/9733ccc395133a067f01ee6e380003d80fe9f443673e0f992ae6a4a7860a872c": [],
211-
"blobs/sha256/61aed1a8baa251dee118b9ab203c1e420f0eda0a9b3f9322d67d235dd27a12ee": [
211+
"blobs/sha256/0e4613a3c620a37d93aca05039001fb5a6063c9d9cfb0935e3aa984025f31198": [
212212
{
213213
"kind": "v1.discover.binary.jar",
214-
"path": "jackson-annotations-2.17.1.jar",
214+
"path": "slf4j-ext-2.0.0.jar",
215215
"fingerprints": {
216-
"v1.mavencentral.jar": "/KfvYZLJrQXQe8UNqZG/k3qErzo=",
217-
"sha_256": "/MrYLhMXLA5DhNtxV3IZybhjHAgg9LGNqqVwFvtmHHY=",
218-
"v1.class.jar": "t2Btr6rNrvzghM5Nud2uldRGVjw0/n5rK9j0xooQQyk=",
219-
"v1.raw.jar": "wjGJk8cvY4tpKcUC5r8YuO15Wfv+rVuyWANBYCUIeDs="
216+
"v1.mavencentral.jar": "WO8bdGURkfUQyqK6rJ4miP9caEU=",
217+
"v1.class.jar": "PexFkKDUkwq7Do2Pt3HVPjMBRfqj/Zzp+nK5D6LfPF4=",
218+
"sha_256": "bWERAhXZlaGR2AxgVJDRTAlbOtHqLIVOpmncfLIUKj0=",
219+
"v1.raw.jar": "7CEXDIU3h2Vcj6lmy4gbuh4KsMVCNgUZTCj9VA4VoV8="
220+
}
221+
}
222+
],
223+
"blobs/sha256/632e84390ad558f9db0524f5e38a0af3e79c623a46bdce8a5e6a1761041b9850": [],
224+
"blobs/sha256/054f94aa7ce72b59cd6abac5462f77f0645b2f1a7b17e55d8f847a6da58c90db": [
225+
{
226+
"kind": "v1.discover.binary.jar",
227+
"path": "inner_directory/commons-email2-jakarta-2.0.0-M1.jar",
228+
"fingerprints": {
229+
"v1.class.jar": "2wRGbMGyGRwEXqNm53h1YK8OO879kvzDxazmJXiAcfI=",
230+
"v1.raw.jar": "QA4SAurtJeo+lx1Vqve5uQYnvDKLFx1NgsSuoWKi8pw=",
231+
"sha_256": "MuEcK3nOFuySTTg4HOJi3vvTpI9bYspfMHa9AK2merQ=",
232+
"v1.mavencentral.jar": "6bzpyKql6Q+UxKQgm14pcP4wHGo="
220233
}
221234
}
222235
]
223236
}
224-
}"#;
237+
}
238+
"#;
225239

226240
#[test]
227241
fn it_finds_expected_output() {

integration-test/Container/AnalysisSpec.hs

+4-4
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ registrySourceAnalysis = do
4848
aroundAll (runAnalyze registrySourceCfg) $ do
4949
describe "Container analysis from registry source" $ do
5050
it "Has the correct OS" $
51-
\res -> res.imageData.imageOs `shouldBe` "alpine"
51+
\scan -> scan.imageData.imageOs `shouldBe` Just "alpine"
5252
it "Has the correct OS release version" $
53-
\res -> res.imageData.imageOsRelease `shouldBe` "3.19.1"
53+
\scan -> scan.imageData.imageOsRelease `shouldBe` Just "3.19.1"
5454
it "Has the expected image tag" $
55-
\res -> res.imageTag `shouldBe` "public.ecr.aws/docker/library/alpine"
55+
\scan -> scan.imageTag `shouldBe` "public.ecr.aws/docker/library/alpine"
5656
it "Has at least one layer" $
57-
\res -> res.imageData.imageLayers `shouldSatisfy` (not . null)
57+
\scan -> scan.imageData.imageLayers `shouldSatisfy` (not . null)

src/App/Fossa/Container/Sources/Discovery.hs

+8-4
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,15 @@ import Types (
5959
FoundTargets (FoundTargets, ProjectWithoutTargets),
6060
)
6161

62-
layerAnalyzers :: AnalyzeStaticTaskEffs sig m => OsInfo -> Bool -> [DiscoverFunc m]
63-
layerAnalyzers os onlySystemDeps =
62+
layerAnalyzers :: AnalyzeStaticTaskEffs sig m => Maybe OsInfo -> Bool -> [DiscoverFunc m]
63+
layerAnalyzers os onlySystemDeps = do
64+
-- For true distroless container support the linux builds need to be able to produce a result without os info.
65+
-- Not having OS info will mean we miss any system deps in the container.
66+
-- It's still useful to allow this though because we can do jar in container analysis as well as regular project analysis.
67+
let systemDeps = maybe [] osDepsAnalyzers os
6468
if onlySystemDeps
65-
then osDepsAnalyzers os
66-
else osDepsAnalyzers os ++ managedDepsDiscoveryF
69+
then systemDeps
70+
else systemDeps ++ managedDepsDiscoveryF
6771

6872
osDepsAnalyzers :: AnalyzeStaticTaskEffs sig m => OsInfo -> [DiscoverFunc m]
6973
osDepsAnalyzers osInfo =

src/App/Fossa/Container/Sources/DockerArchive.hs

+17-9
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import Data.FileTree.IndexFileTree (SomeFileTree, fixedVfsRoot)
6262
import Data.Flag (Flag, fromFlag)
6363
import Data.Foldable (traverse_)
6464
import Data.Map qualified as Map
65-
import Data.Maybe (listToMaybe, mapMaybe)
65+
import Data.Maybe (isNothing, listToMaybe, mapMaybe)
6666
import Data.String.Conversion (ToString (toString))
6767
import Data.Text (Text)
6868
import Data.Text.Extra (breakOnEndAndRemove, showT)
@@ -122,8 +122,13 @@ analyzeFromDockerArchive systemDepsOnly filters withoutDefaultFilters tarball =
122122
let imageBaseLayer = baseLayer image
123123
baseDigest = layerDigest imageBaseLayer
124124
osInfo <-
125-
context "Retrieving OS Information" $
126-
runTarballReadFSIO baseFs tarball getOsInfo
125+
context "Retrieving OS Information"
126+
. warnThenRecover @Text "Could not retrieve OS info"
127+
$ runTarballReadFSIO baseFs tarball getOsInfo
128+
129+
when (isNothing osInfo) $
130+
logInfo "No image system information detected. System dependencies will not be included with this scan."
131+
127132
baseUnits <-
128133
context "Analyzing From Base Layer" $
129134
analyzeLayer systemDepsOnly filters withoutDefaultFilters capabilities osInfo baseFs tarball
@@ -133,8 +138,8 @@ analyzeFromDockerArchive systemDepsOnly filters withoutDefaultFilters tarball =
133138
ContainerScan
134139
{ imageData =
135140
( ContainerScanImage
136-
(nameId osInfo)
137-
(version osInfo)
141+
(nameId <$> osInfo)
142+
(version <$> osInfo)
138143
layers
139144
)
140145
, imageDigest
@@ -185,7 +190,7 @@ analyzeLayer ::
185190
AllFilters ->
186191
Flag WithoutDefaultFilters ->
187192
Int ->
188-
OsInfo ->
193+
Maybe OsInfo ->
189194
SomeFileTree TarEntryOffset ->
190195
Path Abs File ->
191196
m [SourceUnit]
@@ -228,7 +233,7 @@ runAnalyzers ::
228233
, Has AtomicCounter sig m
229234
) =>
230235
Bool ->
231-
OsInfo ->
236+
Maybe OsInfo ->
232237
AllFilters ->
233238
Flag WithoutDefaultFilters ->
234239
m ()
@@ -330,7 +335,10 @@ listTargetsFromDockerArchive tarball = do
330335

331336
logInfo "Analyzing Base Layer"
332337
baseFs <- context "Building Base Layer FS" $ mkFsFromChangeset $ baseLayer image
333-
osInfo <- context "Retrieving OS Information" $ runTarballReadFSIO baseFs tarball getOsInfo
338+
osInfo <-
339+
context "Retrieving OS Information"
340+
. warnThenRecover @Text "Could not retrieve OS info"
341+
$ runTarballReadFSIO baseFs tarball getOsInfo
334342
context "Analyzing From Base Layer" $ listTargetLayer capabilities osInfo baseFs tarball "Base Layer"
335343

336344
when (hasOtherLayers image) $ do
@@ -346,7 +354,7 @@ listTargetLayer ::
346354
, Has Telemetry sig m
347355
) =>
348356
Int ->
349-
OsInfo ->
357+
Maybe OsInfo ->
350358
SomeFileTree TarEntryOffset ->
351359
Path Abs File ->
352360
Text ->

src/Container/Types.hs

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ instance ToJSON ContainerScan where
100100
toJSON scan = object ["image" .= imageData scan]
101101

102102
data ContainerScanImage = ContainerScanImage
103-
{ imageOs :: Text
104-
, imageOsRelease :: Text
103+
{ imageOs :: Maybe Text
104+
, imageOsRelease :: Maybe Text
105105
, imageLayers :: [ContainerScanImageLayer]
106106
}
107107
deriving (Show, Eq, Ord)

test/App/Fossa/Container/AnalyzeNativeSpec.hs

+41-12
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@ import Effect.ReadFS (ReadFSIOC, runReadFSIO)
2727
import Path (Dir, File, Rel, mkRelDir, mkRelFile, (</>))
2828
import Path.IO (getCurrentDir)
2929
import Path.Internal (Path)
30-
import Srclib.Types (
31-
Locator (Locator),
32-
SourceUnit (sourceUnitBuild),
33-
SourceUnitBuild (buildImports),
34-
)
30+
import Srclib.Types (Locator (..), SourceUnit (..), SourceUnitBuild (..), SourceUnitDependency (..), textToOriginPath)
3531
import Test.Effect (
3632
expectFatal',
3733
handleDiag,
@@ -51,10 +47,7 @@ import Test.MockDockerEngineApi (
5147
runMockApi,
5248
)
5349
import Type.Operator (type ($))
54-
import Types (
55-
DiscoveredProjectType (SetuptoolsProjectType),
56-
TargetFilter (TypeDirTarget, TypeTarget),
57-
)
50+
import Types (DiscoveredProjectType (SetuptoolsProjectType), GraphBreadth (..), TargetFilter (TypeDirTarget, TypeTarget))
5851

5952
spec :: Spec
6053
spec = do
@@ -194,8 +187,8 @@ jarsInContainerSpec :: Spec
194187
jarsInContainerSpec = describe "Jars in Containers" $ do
195188
currDir <- runIO getCurrentDir
196189
let imageArchivePath = currDir </> jarsInContainerImage
197-
baseLayerId = "sha256:9733ccc395133a067f01ee6e380003d80fe9f443673e0f992ae6a4a7860a872c"
198-
otherLayerId = "sha256:5c079c30beb013e4b2f7729b6bdce6fba57941d28f20db985333fc1dd969f018"
190+
baseLayerId = "sha256:61aed1a8baa251dee118b9ab203c1e420f0eda0a9b3f9322d67d235dd27a12ee"
191+
otherLayerId = "sha256:632e84390ad558f9db0524f5e38a0af3e79c623a46bdce8a5e6a1761041b9850"
199192

200193
it' "Reads and merges the layers correctly" $ do
201194
ContainerScan{imageData = ContainerScanImage{imageLayers}} <- analyzeFromDockerArchive False mempty (toFlag' False) imageArchivePath
@@ -209,5 +202,41 @@ jarsInContainerSpec = describe "Jars in Containers" $ do
209202
-- The CLI only passes observations along without inspecting them.
210203
-- So this test just checks that the number of them that we expect are there.
211204
-- More specific tests for observation content are in Millhone.
212-
(length <$> Map.lookup baseLayerId observationsMap) `shouldBe'` Just 0
205+
(length <$> Map.lookup baseLayerId observationsMap) `shouldBe'` Just 1
213206
(length <$> Map.lookup otherLayerId observationsMap) `shouldBe'` Just 2
207+
208+
it' "Discovers non-system/non-JIC dependencies" $ do
209+
ContainerScan{imageData = ContainerScanImage{imageLayers}} <- analyzeFromDockerArchive False mempty (toFlag' False) imageArchivePath
210+
211+
let srcUnitsMap = Map.fromList $ map (\layer -> (layerId layer, srcUnits layer)) imageLayers
212+
depLocator =
213+
Locator
214+
{ locatorFetcher = "npm"
215+
, locatorProject = "color-name"
216+
, locatorRevision = Just "2.0.0"
217+
}
218+
expectedSrcUnit =
219+
SourceUnit
220+
{ sourceUnitName = toText $(mkRelDir "./")
221+
, sourceUnitType = "npm"
222+
, sourceUnitManifest = toText $(mkRelDir "./")
223+
, sourceUnitBuild =
224+
Just
225+
SourceUnitBuild
226+
{ buildArtifact = "default"
227+
, buildSucceeded = True
228+
, buildImports =
229+
[depLocator]
230+
, buildDependencies =
231+
[ SourceUnitDependency
232+
{ sourceDepLocator = depLocator
233+
, sourceDepImports = []
234+
}
235+
]
236+
}
237+
, sourceUnitGraphBreadth = Complete
238+
, sourceUnitOriginPaths = [textToOriginPath "package-lock.json"]
239+
, additionalData = Nothing
240+
}
241+
242+
Map.lookup otherLayerId srcUnitsMap `shouldBe'` Just [expectedSrcUnit]

test/App/Fossa/Container/AnalyzeNativeUploadSpec.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ fixtureProjectMetadata :: ProjectMetadata
4545
fixtureProjectMetadata = ProjectMetadata Nothing Nothing Nothing Nothing Nothing Nothing ["label-1", "label-2"] Nothing
4646

4747
fixtureContainerScan :: ContainerScan
48-
fixtureContainerScan = ContainerScan (ContainerScanImage "alpine" "3.1.4" []) "some-digest" "some-tag"
48+
fixtureContainerScan = ContainerScan (ContainerScanImage (Just "alpine") (Just "3.1.4") []) "some-digest" "some-tag"
4949

5050
fixtureRevision :: ProjectRevision
5151
fixtureRevision = ProjectRevision "some-tag" "some-digest" $ Just "master"
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Use this DockerFile to build an image used in the jars in containers tests.
2-
FROM alpine:3.14
2+
FROM scratch
33
# https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.17.1/
44
COPY jackson-annotations-2.17.1.jar .
55
# https://repo1.maven.org/maven2/org/apache/commons/commons-email2-jakarta/2.0.0-M1/
66
COPY commons-email2-jakarta-2.0.0-M1.jar ./inner_directory/
7+
# https://repo1.maven.org/maven2/org/slf4j/slf4j-ext/2.0.0/
8+
COPY slf4j-ext-2.0.0.jar .
9+
# This sample project is in the current directory.
10+
COPY tiny-project .
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:c7a7c625c39b39900428d0a68a2f2330991a9298d90453968de6d32b4ffba2a4
3-
size 6047232
2+
oid sha256:cf00a8861519fd2357380e66e060f9080d5e3ec99e4b8f7feeeaf6b11bbfbb62
3+
size 197120

test/App/Fossa/Container/testdata/tiny-project/package-lock.json

+24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "tiny-project",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"author": "",
10+
"license": "ISC",
11+
"dependencies": {
12+
"color-name": "^2.0.0"
13+
}
14+
}

0 commit comments

Comments
 (0)