Skip to content

[No Ticket] Stricter redirects when interacting with registry v2 api #1308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# FOSSA CLI Changelog

## Unreleased
- container scanning: Fixes a network error defect, when interacting with container registry redirects. ([#1308](https://github.com/fossas/fossa-cli/pull/1308/))

## v3.8.20
- container scanning: Fixes registry network calls, to ensure `fossa-cli` uses `Accept` header on `HEAD` network calls. ([#1309](https://github.com/fossas/fossa-cli/pull/1309))

Expand Down
116 changes: 116 additions & 0 deletions integration-test/Analysis/ContainerScanningSpec.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{-# LANGUAGE DataKinds #-}

module Analysis.ContainerScanningSpec (spec) where

import Container.Docker.OciManifest (OciManifestConfig (..), OciManifestV2 (..))
import Container.Docker.SourceParser (
RegistryImageSource,
RepoDigest (..),
parseImageUrl,
)
import Control.Algebra (Has)
import Control.Carrier.ContainerRegistryApi (runContainerRegistryApi)
import Control.Carrier.ContainerRegistryApi.Common (RegistryCtx)
import Control.Carrier.Diagnostics (
DiagnosticsC,
fromEitherShow,
runDiagnostics,
)
import Control.Carrier.Finally (FinallyC, runFinally)
import Control.Carrier.Lift (Lift)
import Control.Carrier.Reader (ReaderC, runReader)
import Control.Carrier.Simple (SimpleC)
import Control.Carrier.Stack (StackC, runStack)
import Control.Carrier.StickyLogger (
IgnoreStickyLoggerC,
ignoreStickyLogger,
)
import Control.Effect.ContainerRegistryApi (
ContainerRegistryApi,
ContainerRegistryApiF,
getImageManifest,
)
import Control.Effect.Diagnostics (Diagnostics)
import Control.Effect.Lift (sendIO)
import Data.String.Conversion (toText)
import Data.Text (Text)
import Data.Void (Void)
import Diag.Result (Result (..), renderFailure)
import Discovery.Filters (AllFilters)
import Effect.Exec (ExecIOC, runExecIO)
import Effect.Logger (IgnoreLoggerC, ignoreLogger)
import Effect.ReadFS (ReadFSIOC, runReadFSIO)
import System.Environment (lookupEnv)
import Test.Hspec (Spec, describe, it, shouldBe)
import Text.Megaparsec (ParseErrorBundle, parse)
import Type.Operator (type ($))

spec :: Spec
spec = testPrivateRepos

-- Integeration tests to catch, if registry api implementation changes
-- abruptly.
testPrivateRepos :: Spec
testPrivateRepos =
describe "private registries" $ do
it "dockerhub" $ do
-- Refer to 1Password for creds or Github Action Env Keys
img <- withAuth "FOSSA_DOCKERHUB_WWW_STYLE_USER_PASS" dockerHubImage
res <- runEff $ getImageConfig "amd64" img
case res of
Failure ws eg -> fail (show (renderFailure ws eg "An issue occurred"))
Success _ res' -> res' `shouldBe` dockerHubImageConfigDigest

dockerHubImage :: Text
dockerHubImage = "index.docker.io/fossabot/container-test-fixture:0"

dockerHubImageConfigDigest :: RepoDigest
dockerHubImageConfigDigest = RepoDigest "sha256:792d29ec0ccee20718b0233b1ca0c633a57009bbb4d99c247b0ec1e3f562b19b"

--

type EffectStack m =
FinallyC
$ SimpleC ContainerRegistryApiF
$ ReaderC RegistryCtx
$ ReaderC AllFilters
$ ExecIOC
$ ReadFSIOC
$ DiagnosticsC
$ IgnoreLoggerC
$ IgnoreStickyLoggerC
$ StackC IO

runEff :: EffectStack IO a -> IO (Result a)
runEff =
runStack
. ignoreStickyLogger
. ignoreLogger
. runDiagnostics
. runReadFSIO
. runExecIO
. runReader mempty
. runContainerRegistryApi
. runFinally

decodeStrict :: Text -> Text -> Either (ParseErrorBundle Text Void) RegistryImageSource
decodeStrict arch = parse (parseImageUrl arch) mempty

getImageConfig ::
( Has (Lift IO) sig m
, Has Diagnostics sig m
, Has ContainerRegistryApi sig m
) =>
Text ->
Text ->
m RepoDigest
getImageConfig arch img =
configDigest . ociConfig
<$> (getImageManifest =<< fromEitherShow (decodeStrict arch img))

withAuth :: Has (Lift IO) sig m => String -> Text -> m Text
withAuth authEnvKey target = do
auth <- sendIO $ lookupEnv authEnvKey
case auth of
Nothing -> pure target
Just auth' -> pure $ (toText auth') <> "@" <> target
1 change: 1 addition & 0 deletions spectrometer.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,7 @@ test-suite integration-tests
Analysis.CarthageSpec
Analysis.ClojureSpec
Analysis.CocoapodsSpec
Analysis.ContainerScanningSpec
Analysis.ElixirSpec
Analysis.ErlangSpec
Analysis.FixtureExpectationUtils
Expand Down
2 changes: 1 addition & 1 deletion src/Container/Docker/SourceParser.hs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ parseAuthCred :: Parser (Text, Text)
parseAuthCred = do
user <- toText <$> some alphaNumChar
void (char ':')
password <- toText <$> (some alphaNumChar)
password <- toText <$> some (alphaNumChar <|> char '_' <|> char '-')
void (char '@')
pure (user, password)

Expand Down
19 changes: 5 additions & 14 deletions src/Control/Carrier/ContainerRegistryApi/Authorization.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,20 @@ import Data.Aeson (FromJSON (parseJSON), decode', eitherDecode, withObject, (.:)
import Data.ByteString.Lazy qualified as ByteStringLazy
import Data.Map (Map)
import Data.Map qualified as Map
import Data.String.Conversion (ConvertUtf8 (decodeUtf8), encodeUtf8, toString, toText)
import Data.Text (Text, isInfixOf)
import Data.String.Conversion (encodeUtf8, toString, toText)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Void (Void)
import Effect.Logger (Logger)
import Network.HTTP.Client (
Manager,
Request (host, method, shouldStripHeaderOnRedirect),
Request (method, shouldStripHeaderOnRedirect),
Response (responseBody, responseHeaders, responseStatus),
applyBasicAuth,
applyBearerAuth,
parseRequest,
)
import Network.HTTP.Types (methodGet, statusCode)
import Network.HTTP.Types (statusCode)
import Network.HTTP.Types.Header (
hAuthorization,
hWWWAuthenticate,
Expand Down Expand Up @@ -99,16 +99,7 @@ applyAuthToken (Just (BearerAuthToken token)) r =
-- If we don't strip auth headers, on redirect, depending on how
-- blobs/manifest are retrieved cloud vendor may throw 'Bad Request' error.
stripAuthHeaderOnRedirect :: Request -> Request
stripAuthHeaderOnRedirect r =
if ((isAwsECR || isAzure) && method r == methodGet)
then r{shouldStripHeaderOnRedirect = (== hAuthorization)}
else r
where
isAwsECR :: Bool
isAwsECR = "amazonaws.com" `isInfixOf` decodeUtf8 (host r)

isAzure :: Bool
isAzure = "azurecr.io" `isInfixOf` decodeUtf8 (host r)
stripAuthHeaderOnRedirect r = r{shouldStripHeaderOnRedirect = (== hAuthorization)}

-- | Generates Auth Token For Request.
--
Expand Down
10 changes: 10 additions & 0 deletions test/Container/Docker/SourceParserSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ spec = do
fixtureArch
)

"https://user:[email protected]/fossas/haskell-dev-tools:9.0.2"
`shouldParseInto` ( RegistryImageSource
"ghcr.io"
defaultHttpScheme
(Just ("user", "pass-pass_pass"))
"fossas/haskell-dev-tools"
(mkTagRef "9.0.2")
fixtureArch
)

fixtureArch :: Text
fixtureArch = "amd64"

Expand Down