daml/security/EvidenceSecurity.hs
Claudio Bley e766f91338
Generate CSV file from all test evidences for ledger client components (#15372)
* Add new security categories

* Use test-security framework for OAuth2 middleware

* Regenerate `security-evidence.md`

* Explicitely exit the test-evidence generator

* Use test-security framework for HTTP JSON

* Regenerate `security-evidence.md`

* Add //ledger-service/http-json:integration-tests-lib to evidence generator

* Skip maven artefacts on the classpath

The checker framework artefact (`checker-2.5.4.jar`) causes an `IllegalAccessError` when included in the runpath:
```
Exception in thread "main" java.lang.IllegalAccessError: class com.sun.tools.javac.code.Scope$ImportScope$ImportEntry cannot access its superclass com.sun.tools.javac.code.Scope$Entry (com.sun.tools.javac.code.Scope$ImportScope$ImportEntry is in unnamed module of loader java.net.URLClassLoader @31000e60; com.sun.tools.javac.code.Scope$Entry is in module jdk.compiler of loader app)
	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
	at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
	at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
	at java.base/java.net.URLClassLoader.defineClass(URLClassLoader.java:550)
	at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:458)
	at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:452)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:451)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
	at org.scalatest.tools.SuiteDiscoveryHelper$.isDiscoverableSuite(SuiteDiscoveryHelper.scala:204)
	at org.scalatest.tools.SuiteDiscoveryHelper$.processClassName(SuiteDiscoveryHelper.scala:243)
	at org.scalatest.tools.SuiteDiscoveryHelper$.$anonfun$processFileNames$1(SuiteDiscoveryHelper.scala:279)
	at scala.collection.Iterator$$anon$9.next(Iterator.scala:577)
	at scala.collection.Iterator$$anon$6.hasNext(Iterator.scala:474)
	at scala.collection.Iterator$$anon$9.hasNext(Iterator.scala:576)
	at scala.collection.immutable.List.prependedAll(List.scala:152)
	at scala.collection.immutable.List$.from(List.scala:684)
	at scala.collection.immutable.List$.from(List.scala:681)
	at scala.collection.IterableFactory$Delegate.from(Factory.scala:288)
	at scala.collection.immutable.Iterable$.from(Iterable.scala:35)
	at scala.collection.immutable.Iterable$.from(Iterable.scala:32)
	at scala.collection.IterableFactory$Delegate.from(Factory.scala:288)
	at scala.collection.IterableOnceExtensionMethods$.toIterable$extension(IterableOnce.scala:178)
	at org.scalatest.tools.SuiteDiscoveryHelper$.processFileNames(SuiteDiscoveryHelper.scala:285)
	at org.scalatest.tools.SuiteDiscoveryHelper$.$anonfun$discoverSuiteNames$1(SuiteDiscoveryHelper.scala:132)
	at scala.collection.immutable.List.map(List.scala:250)
```

* Regenerate `security-evidence.md`

* Convert remaining TEST_EVIDENCE stanzas of HTTP JSON

* Regenerate `security-evidence.md`

* Use test-security framework for HTTP JSON

* Regenerate `security-evidence.md`

* Print warning when a test suite could not be loaded

* Fix typo

* Use test-security framework for HTTP JSON

* Read files in tests lazily

The test-evidence generator tool needs to instantiate scalatest test suites in order
to access the tagged tests and collect relavant test entries.

* Use test-security framework for HTTP JSON

* Regenerate `security-evidence.md`

* Regenerate `security-evidence.md`

* Use test-security framework for HTTP JSON

* Regenerate `security-evidence.md`

* Use test-security framework for HTTP JSON

* Regenerate `security-evidence.md`

* Use test-security framework for HTTP JSON

* Regenerate `security-evidence.md`

* Use test-security framework for trigger service

* Use structural type to call `in` and `ignore` for different classes

* Remove Authentication category from EvidenceSecurity tool

There are no TEST_EVIDENCE annotations anymore.

* Add required trigger-runner-lib


* Import `scala.language.reflectiveCalls` where it is needed

* Remove left-over comments

* Add `test_evidence_binary` scala binary rule

This rule is a customized `scala_binary` rule which also accepts a `tests` attr and
generates a runpath file which is later consumed by scalatest to detect the relevant
scalatest test suites.

* Process test suites, add transitive deps

* Support nested tests_suites in `test_evidence_binary`

* Remove debug print's

* Add missing dependencies to test-evidence:generator

* Abort if test suites cannot be loaded

* Cleanup

* Reinstate scalacopts in http-json

* Reword the test description to not drop information

* Fix typo

* Explicitly exit the JVM on exceptions

This is required since non-daemon threads also prevent JVM shutdown when an exception was thrown.

* Format test-evidence/BUILD.bazel

* Resolve file paths lazily

This avoids a `NullPointerException` on Windows where Runfiles.rlocation returns `null`.

* Document new Security properties

* Print target directory and file name

* Clarify test descriptions

* Replace duplicate Security properties

Co-authored-by: Stephen Compall <stephen.compall@daml.com>Co-authored-by: Stephen Compall <stephen.compall@daml.com>
2022-11-29 12:35:22 +01:00

151 lines
4.6 KiB
Haskell

-- Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module Main (main) where
import Control.Monad (when,void)
import Data.List ((\\),sortOn)
import Data.List.Extra (groupOn,foldl')
import Data.Map (Map)
import Data.Text (Text)
import Data.Void (Void)
import System.Exit (exitWith,ExitCode(ExitFailure))
import System.FilePath (splitPath)
import System.IO.Extra (hPutStrLn,stderr)
import Text.Megaparsec (Parsec,runParser,errorBundlePretty,eof,takeWhileP,single,label,satisfy,noneOf,chunk,(<|>),some)
import qualified Text.Megaparsec.Char (space)
import qualified Data.Char as Char (isDigit,digitToInt)
import qualified Data.Map as Map (fromList,toList)
import qualified Data.Text as T (pack,unpack)
import qualified Data.Text.IO as T (getContents)
{-
Generate _security evidence_ by documenting _security_ test cases.
Security tests may be found anywhere in the Daml repository, and written in any language
(scala, haskell, shell, etc). They are marked by the *magic comment*: "TEST_EVIDENCE"
followed by a ":".
Following the marker, the remaining text on the line is split on the next ":" to give:
Category : Free text description of the test case.
There are a fixed set of categories, listed in the enum below. There expect at least one
testcase for every category.
The generated evidence is a markdown file, listing each testcase, grouped by Category. For
each testcase we note the free-text with a link to the line in the original file.
This program is expected to be run with stdin generated by a git grep command, and stdout
redirected to the name of the generated file:
```
git grep --line-number TEST_EVIDENCE\: | bazel run security:evidence-security > security-evidence.md
```
-}
main :: IO ()
main = do
text <- T.getContents
lines <- parseLines text
let missingCats = [minBound..maxBound] \\ [ cat | Line{cat} <- lines ]
when (not $ null missingCats) $ do
messageAndExitFail ("No tests for categories: " ++ show missingCats)
putStrLn (ppCollated (collateLines lines))
type Parser = Parsec Void Text
parseLines :: Text -> IO [Line]
parseLines text = do
case runParser theParser "<stdin>" text of
Right xs -> pure xs
Left e -> messageAndExitFail $ errorBundlePretty e
messageAndExitFail :: String -> IO a
messageAndExitFail message = do
hPutStrLn stderr "** EvidenceSecurity: generation failed:"
hPutStrLn stderr message
exitWith $ ExitFailure 1
theParser :: Parser [Line]
theParser = some line <* eof
where
line = do
filename <- some notColonOrNewline
colon
lineno <- number
colon
marker
colon
optWhiteSpace
cat <- parseCategory
colon
optWhiteSpace
freeText <- takeWhileP (Just "freetext") (/= '\n')
void $ single '\n'
pure Line {cat, desc = Description{filename,lineno,freeText}}
number = foldl' (\acc d -> 10*acc+d) 0 <$> some digit
digit = label "digit" $ Char.digitToInt <$> satisfy Char.isDigit
marker =
(void $ chunk "TEST_EVIDENCE")
<|> do void notColonOrNewline; marker
optWhiteSpace = Text.Megaparsec.Char.space
parseCategory = do
foldl1 (<|>)
[ do void $ chunk $ T.pack $ ppCategory cat; pure cat
| cat <- [minBound..maxBound]
]
colon = void $ single ':'
notColonOrNewline = noneOf [':','\n']
data Category
= Authorization
| Availability
| Confidentiality
| Integrity
deriving (Eq,Ord,Bounded,Enum,Show)
data Description = Description
{ filename:: FilePath
, lineno:: Int
, freeText:: Text
}
data Line = Line { cat :: Category, desc :: Description }
newtype Collated = Collated (Map Category [Description])
collateLines :: [Line] -> Collated
collateLines lines =
Collated $ Map.fromList
[ (cat, [ desc | Line{desc} <- group ])
| group@(Line{cat}:_) <- groupOn cat (sortOn cat lines)
]
ppCollated :: Collated -> String
ppCollated (Collated m) =
unlines (["# Security tests, by category",""] ++
[ unlines (("## " ++ ppCategory cat ++ ":") : map ppDescription (sortOn freeText descs))
| (cat,descs) <- sortOn fst (Map.toList m)
])
ppDescription :: Description -> String
ppDescription Description{filename,lineno,freeText} =
"- " ++ T.unpack freeText ++ ": [" ++ basename filename ++ "](" ++ filename ++ "#L" ++ show lineno ++ ")"
where
basename :: FilePath -> FilePath
basename p = case reverse (splitPath p) of [] -> ""; x:_ -> x
ppCategory :: Category -> String
ppCategory = \case
Authorization -> "Authorization"
Availability -> "Availability"
Confidentiality -> "Confidentiality"
Integrity -> "Integrity"