cef (empty) → 0.1.0.0
raw patch · 6 files changed
+689/−0 lines, 6 filesdep +basedep +bytestringdep +directorybuild-type:Customsetup-changed
Dependencies added: base, bytestring, directory, doctest, filepath, text, time
Files
- LICENSE +30/−0
- Setup.lhs +39/−0
- cef.cabal +33/−0
- src/System/Log/CEF.hs +75/−0
- src/System/Log/CEF/Extensions.hs +481/−0
- test/doctests.hs +31/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2015, Picus Security++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Picus Security nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.lhs view
@@ -0,0 +1,39 @@+#!/usr/bin/runhaskell+\begin{code}+{-# OPTIONS_GHC -Wall #-}+module Main (main) where+import Data.List ( nub )+import Data.Version ( showVersion )+import Distribution.Package ( PackageName(PackageName), PackageId, InstalledPackageId, packageVersion, packageName )+import Distribution.PackageDescription ( PackageDescription(), TestSuite(..) )+import Distribution.Simple ( defaultMainWithHooks, UserHooks(..), simpleUserHooks )+import Distribution.Simple.Utils ( rewriteFile, createDirectoryIfMissingVerbose )+import Distribution.Simple.BuildPaths ( autogenModulesDir )+import Distribution.Simple.Setup ( BuildFlags(buildVerbosity), fromFlag )+import Distribution.Simple.LocalBuildInfo ( withLibLBI, withTestLBI, LocalBuildInfo(), ComponentLocalBuildInfo(componentPackageDeps) )+import Distribution.Verbosity ( Verbosity )+import System.FilePath ( (</>) )+main :: IO ()+main = defaultMainWithHooks simpleUserHooks+ { buildHook = \pkg lbi hooks flags -> do+ generateBuildModule (fromFlag (buildVerbosity flags)) pkg lbi+ buildHook simpleUserHooks pkg lbi hooks flags+ }+generateBuildModule :: Verbosity -> PackageDescription -> LocalBuildInfo -> IO ()+generateBuildModule verbosity pkg lbi = do+ let dir = autogenModulesDir lbi+ createDirectoryIfMissingVerbose verbosity True dir+ withLibLBI pkg lbi $ \_ libcfg -> do+ withTestLBI pkg lbi $ \suite suitecfg -> do+ rewriteFile (dir </> "Build_" ++ testName suite ++ ".hs") $ unlines+ [ "module Build_" ++ testName suite ++ " where"+ , "deps :: [String]"+ , "deps = " ++ (show $ formatdeps (testDeps libcfg suitecfg))+ ]+ where+ formatdeps = map (formatone . snd)+ formatone p = case packageName p of+ PackageName n -> n ++ "-" ++ showVersion (packageVersion p)+testDeps :: ComponentLocalBuildInfo -> ComponentLocalBuildInfo -> [(InstalledPackageId, PackageId)]+testDeps xs ys = nub $ componentPackageDeps xs ++ componentPackageDeps ys+\end{code}
+ cef.cabal view
@@ -0,0 +1,33 @@+name: cef+version: 0.1.0.0+synopsis: CEF log format+description: This library is used to produce CEF log lines+homepage: http://github.com/picussecurity/haskell-cef.git+license: BSD3+license-file: LICENSE+author: Picus Security+maintainer: utdemir@picussecurity.com+category: System+build-type: Custom+cabal-version: >=1.10++library+ exposed-modules: System.Log.CEF+ other-modules: System.Log.CEF.Extensions+ build-depends: base >=4.7 && <4.9+ , text+ , bytestring+ , time+ hs-source-dirs: src+ default-language: Haskell2010++test-suite doctests+ type: exitcode-stdio-1.0+ main-is: doctests.hs+ build-depends: base+ , doctest >= 0.9.1+ , directory+ , filepath+ ghc-options: -Wall -threaded+ hs-source-dirs: test+ default-language: Haskell2010
+ src/System/Log/CEF.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- | CEF Log Format+--+-- This library implements ArcSight CEF Revision 20 released on 06/05/2013+--+-- See: <https://protect724.hp.com/servlet/JiveServlet/downloadBody/1072-102-6-4697/CommonEventFormat.pdf>++module System.Log.CEF+ ( CEFEvent (..)+ , log+ -- * Extensions+ , module System.Log.CEF.Extensions+ ) where++--------------------------------------------------------------------------------+import Data.ByteString.Builder+import qualified Data.ByteString.Lazy as BL+import Data.Foldable+import Data.List (intersperse)+import Data.Monoid+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Prelude hiding (log)+--------------------------------------------------------------------------------+import System.Log.CEF.Extensions+--------------------------------------------------------------------------------++-- Cef log line:+-- CEF:Version|Device Vendor|Device Product|Device|Version|Signature ID|Name|Severity|[Extension]++data CEFEvent+ = CEFEvent { deviceVendor :: T.Text+ , deviceProduct :: T.Text+ , deviceVersion :: T.Text+ , signatureId :: T.Text -- ^ Is a unique identifier per event-type.+ , name :: T.Text -- ^ Is a string representing a human-readable and understandable description of the event.+ , severity :: Int -- ^ Reflects the importance of the event. Must be in range @[0..10]@.+ , extensions :: Extensions+ }+++-- |+-- >>> :set -XOverloadedStrings+-- >>> let exampleEvent = CEFEvent "Acme Corp" "Acmetorazor" "2.1" "cool" "MyNameIsCool" 10 (applicationProtocol "PUT")+-- >>> toLazyByteString $ log exampleEvent+-- "CEF:0|Acme Corp|Acmetorazor|2.1|MyNameIsCool|10|app=PUT\n"+log :: CEFEvent -> Builder+log CEFEvent{..} = mconcat (intersperse sep fields) <> "\n"+ where sep = "|"+ fields = [ "CEF:0"+ , renderHeader deviceVendor+ , renderHeader deviceProduct+ , renderHeader deviceVersion+ , renderHeader name+ , intDec severity+ , extensionsBuilder extensions+ ]++-- |+-- >>> toLazyByteString (renderHeader "")+-- ""+-- >>> toLazyByteString (renderHeader "foo|bar")+-- "foo\\|bar"+-- >>> toLazyByteString (renderHeader "foo\\bar")+-- "foo\\\\bar"+renderHeader :: T.Text -> Builder+renderHeader cs = T.encodeUtf8Builder $ T.concatMap escape cs+ where escape '|' = "\\|"+ escape '\\' = "\\\\"+ escape i = T.singleton i+++
+ src/System/Log/CEF/Extensions.hs view
@@ -0,0 +1,481 @@+{-# LANGUAGE OverloadedStrings #-}++module System.Log.CEF.Extensions+ ( Extensions+ , emptyExtensions+ , extensionsBuilder+ , customExtension+ -- ** Predefined Extensions+ -- | See /Chapter 2: ArcSight Extension Dictionary/ from the reference+ , IPv6Address+ , IPv4Address+ , MACAddress+ , TimeStamp+ , deviceAction+ , deviceCustomIPv6Address1+ , deviceCustomIPv6Address2+ , deviceCustomIPv6Address3+ , deviceCustomIPv6Address4+ , applicationProtocol+ , deviceEventCategory+ , deviceCustomFloatingPoint1+ , deviceCustomFloatingPoint2+ , deviceCustomFloatingPoint3+ , deviceCustomFloatingPoint4+ , deviceCustomNumber1+ , deviceCustomNumber2+ , deviceCustomNumber3+ , deviceCustomNumber4+ , baseEventCount+ , deviceCustomString1+ , deviceCustomString2+ , deviceCustomString3+ , deviceCustomString4+ , deviceCustomString5+ , deviceCustomString6+ , destinationDnsDomain+ , destinationServiceName+ , destinationTranslatedAddress+ , destinationTranslatedPort+ , deviceCustomDate1+ , deviceCustomDate2+ , deviceDirectionInbound+ , deviceDirectionOutbound+ , deviceDnsDomain+ , deviceExternalId+ , deviceFacility+ , deviceInboundInterface+ , deviceMacAddress+ , deviceNtDomain+ , deviceOutboundInterface+ , deviceProcessName+ , deviceTranslatedAddress+ , destinationHostName+ , destinationMacAddress+ , destinationNtDomain+ , destinationProcessId+ , destinationUserPrivileges+ , destinationProcessName+ , destinationPort+ , destinationAddress+ , destinationUserId+ , destinationUserName+ , deviceAddress+ , deviceHostName+ , deviceProcessId+ , endTime+ , externalId+ , fileCreateTime+ , fileHash+ , fileId+ , fileModificationTime+ , filePath+ , filePermission+ , fileType+ , fileName+ , fileSize+ , bytesIn+ , message+ , oldFileCreateTime+ , oldFileHash+ , oldFileId+ , oldFileModificationTime+ , oldFileName+ , oldFilePath+ , oldFilePermission+ , oldFileSize+ , oldFileType+ , bytesOut+ , eventOutcome+ , transportProtocol+ , reason+ , requestURL+ , requestClientApplication+ , requestCookies+ , requestMethod+ , receiptTime+ , sourceHostName+ , sourceMacAddress+ , sourceNtDomain+ , sourceDnsDomain+ , sourceServiceName+ , sourceTranslatedAddress+ , sourceTranslatedPort+ , sourceProcessId+ , sourceUserPrivileges+ , sourceProcessName+ , sourcePort+ , sourceAddress+ , startTime+ , sourceUserId+ , sourceUserName+ ) where++--------------------------------------------------------------------------------+import Data.ByteString.Builder+import Data.Function+import Data.List (intersperse)+import Data.Monoid+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Time.Clock+import Data.Time.Clock.POSIX+--------------------------------------------------------------------------------++newtype Extensions = Extensions { unExtensions :: Builder }++instance Monoid Extensions where+ mempty = emptyExtensions+ {-# INLINE mappend #-}+ Extensions e1 `mappend` Extensions e2 = Extensions (e1 <> " " <> e2)++emptyExtensions :: Extensions+emptyExtensions = mempty++-- |+-- >>> :set -XOverloadedStrings+-- >>> toLazyByteString $ extensionsBuilder (applicationProtocol "PUT" <> deviceCustomIPv6Address1 "localnet" "::1")+-- "app=PUT c6a1Label=localnet c6a1=::1"+extensionsBuilder :: Extensions -> Builder+extensionsBuilder = unExtensions++{-# INLINE ext #-}+ext :: T.Text -> Builder -> Extensions+ext k v = Extensions (T.encodeUtf8Builder k <> "=" <> v)++-- | See /Chapter 4: User-Defined Extensions/ from the reference+customExtension :: T.Text -> T.Text -> Extensions+customExtension k = ext k . renderExtVal++renderExtVal :: T.Text -> Builder+renderExtVal = T.encodeUtf8Builder . T.concatMap go+ where go '\\' = "\\\\"+ go '=' = "\\="+ go '\r' = "\\r"+ go '\n' = "\\n"+ go i = T.singleton i++type IPv6Address = T.Text+type IPv4Address = T.Text+type MACAddress = T.Text+type TimeStamp = UTCTime++utcTimeBuilder :: UTCTime -> Builder+utcTimeBuilder = intDec . fromInteger . round . (* 1000) . utcTimeToPOSIXSeconds++-- Max: 63 Character+deviceAction :: T.Text -> Extensions+deviceAction = customExtension "act"++-- Max: 31 Character+applicationProtocol :: T.Text -> Extensions+applicationProtocol = customExtension "app"++deviceCustomIPv6Address1, deviceCustomIPv6Address2, deviceCustomIPv6Address3, deviceCustomIPv6Address4+ :: T.Text -> IPv6Address -> Extensions+deviceCustomIPv6Address1 t a = customExtension "c6a1Label" t <> customExtension "c6a1" a+deviceCustomIPv6Address2 t a = customExtension "c6a2Label" t <> customExtension "c6a2" a+deviceCustomIPv6Address3 t a = customExtension "c6a3Label" t <> customExtension "c6a3" a+deviceCustomIPv6Address4 t a = customExtension "c6a4Label" t <> customExtension "c6a4" a++-- Max: 1023 Character+deviceEventCategory :: T.Text -> Extensions+deviceEventCategory = customExtension "cat"++deviceCustomFloatingPoint1, deviceCustomFloatingPoint2, deviceCustomFloatingPoint3, deviceCustomFloatingPoint4+ :: T.Text -> Double -> Extensions+deviceCustomFloatingPoint1 t a = customExtension "cfp1Label" t <> ext "cfp1" (doubleDec a)+deviceCustomFloatingPoint2 t a = customExtension "cfp2Label" t <> ext "cfp2" (doubleDec a)+deviceCustomFloatingPoint3 t a = customExtension "cfp3Label" t <> ext "cfp3" (doubleDec a)+deviceCustomFloatingPoint4 t a = customExtension "cfp4Label" t <> ext "cfp4" (doubleDec a)++-- Max: 1023 Character for labels+deviceCustomNumber1, deviceCustomNumber2, deviceCustomNumber3, deviceCustomNumber4+ :: T.Text -> Int -> Extensions+deviceCustomNumber1 t a = customExtension "cn1Label" t <> ext "cn1" (intDec a)+deviceCustomNumber2 t a = customExtension "cn2Label" t <> ext "cn2" (intDec a)+deviceCustomNumber3 t a = customExtension "cn3Label" t <> ext "cn3" (intDec a)+deviceCustomNumber4 t a = customExtension "cn4Label" t <> ext "cn4" (intDec a)++baseEventCount :: Int -> Extensions+baseEventCount = ext "cnt" . intDec++-- Max: 1023 Character for labels+deviceCustomString1, deviceCustomString2, deviceCustomString3, deviceCustomString4, deviceCustomString5, deviceCustomString6+ :: T.Text -> T.Text -> Extensions+deviceCustomString1 t a = customExtension "cs1Label" t <> customExtension "cs1" a+deviceCustomString2 t a = customExtension "cs2Label" t <> customExtension "cs2" a+deviceCustomString3 t a = customExtension "cs3Label" t <> customExtension "cs3" a+deviceCustomString4 t a = customExtension "cs4Label" t <> customExtension "cs4" a+deviceCustomString5 t a = customExtension "cs5Label" t <> customExtension "cs5" a+deviceCustomString6 t a = customExtension "cs6Label" t <> customExtension "cs6" a++destinationDnsDomain :: T.Text -> Extensions+destinationDnsDomain = customExtension "destinationDnsDomain"++destinationServiceName :: T.Text -> Extensions+destinationServiceName = customExtension "destinationServiceName"++destinationTranslatedAddress :: IPv4Address -> Extensions+destinationTranslatedAddress = customExtension "destinationTranslatedAddress"++destinationTranslatedPort :: Int -> Extensions+destinationTranslatedPort = ext "deviceTranslatedPort" . intDec++-- Max: 1023 Character for labels+deviceCustomDate1, deviceCustomDate2 :: T.Text -> TimeStamp -> Extensions+deviceCustomDate1 t a = customExtension "deviceCustomDate1Label" t <> ext "deviceCustomDate1" (utcTimeBuilder a)+deviceCustomDate2 t a = customExtension "deviceCustomDate2Label" t <> ext "deviceCustomDate2" (utcTimeBuilder a)++deviceDirectionInbound, deviceDirectionOutbound :: Extensions+deviceDirectionInbound = ext "deviceDirection" (intDec 0)+deviceDirectionOutbound = ext "deviceDirection" (intDec 1)++-- Max: 255 Character+deviceDnsDomain :: T.Text -> Extensions+deviceDnsDomain = customExtension "deviceDnsDomain"++-- Max: 255 Character+deviceExternalId :: T.Text -> Extensions+deviceExternalId = customExtension "deviceExternalId"++-- Max: 1023 Character+deviceFacility :: T.Text -> Extensions+deviceFacility = customExtension "deviceFacility"++-- Max: 15 Character+deviceInboundInterface :: T.Text -> Extensions+deviceInboundInterface = customExtension "deviceInboundInterface"++deviceMacAddress :: MACAddress -> Extensions+deviceMacAddress = customExtension "deviceMacAddress"++-- Max: 255 Character+deviceNtDomain :: T.Text -> Extensions+deviceNtDomain = customExtension "deviceNtDomain"++-- Max: 15 Character+deviceOutboundInterface :: T.Text -> Extensions+deviceOutboundInterface = customExtension "deviceOutboundInterface"++-- Max: 1023 Character+deviceProcessName :: T.Text -> Extensions+deviceProcessName = customExtension "deviceProcessName"++deviceTranslatedAddress :: IPv4Address -> Extensions+deviceTranslatedAddress = customExtension "deviceTranslatedAddress"++-- Max: 1023 Character+destinationHostName :: T.Text -> Extensions+destinationHostName = customExtension "dhost"++destinationMacAddress :: MACAddress -> Extensions+destinationMacAddress = customExtension "dmac"++-- Max: 255 Character+destinationNtDomain :: T.Text -> Extensions+destinationNtDomain = customExtension "dntdom"++destinationProcessId :: T.Text -> Extensions+destinationProcessId = customExtension "dpid"++-- Max: 1023 Character+destinationUserPrivileges :: T.Text -> Extensions+destinationUserPrivileges = customExtension "dpriv"++-- Max: 1023 Character+destinationProcessName :: T.Text -> Extensions+destinationProcessName = customExtension "dproc"++destinationPort :: Int -> Extensions+destinationPort = ext "dpt" . intDec++destinationAddress :: IPv4Address -> Extensions+destinationAddress = customExtension "dst"++-- Max: 1023 Character+destinationUserId :: T.Text -> Extensions+destinationUserId = customExtension "duid"++-- Max: 1023 Character+destinationUserName :: T.Text -> Extensions+destinationUserName = customExtension "duser"++deviceAddress :: IPv4Address -> Extensions+deviceAddress = customExtension "dvc"++-- Max: 100 Character+deviceHostName :: T.Text -> Extensions+deviceHostName = customExtension "dvchost"++deviceProcessId :: T.Text -> Extensions+deviceProcessId = customExtension "dvcpid"++endTime :: TimeStamp -> Extensions+endTime = ext "end" . utcTimeBuilder++-- Max: 40 Character+externalId :: T.Text -> Extensions+externalId = customExtension "externalId"++fileCreateTime :: TimeStamp -> Extensions+fileCreateTime = ext "fileCreateTime" . utcTimeBuilder++-- Max: 255 Character+fileHash :: T.Text -> Extensions+fileHash = customExtension "fileHash"++-- Max: 1023 Character+fileId :: T.Text -> Extensions+fileId = customExtension "fileId"++fileModificationTime :: TimeStamp -> Extensions+fileModificationTime = ext "fileModificationTime" . utcTimeBuilder++-- Max: 1023 Character+filePath :: T.Text -> Extensions+filePath = customExtension "filePath"++-- Max: 1023 Character+filePermission :: T.Text -> Extensions+filePermission = customExtension "filePermission"++-- Max: 1023 Character+fileType :: T.Text -> Extensions+fileType = customExtension "fileType"++-- Max: 1023 Character+fileName :: T.Text -> Extensions+fileName = customExtension "fileName"++fileSize :: Int -> Extensions+fileSize = ext "fileSize" . intDec++bytesIn :: Int -> Extensions+bytesIn = ext "in" . intDec++-- Max: 1023 Character+message :: T.Text -> Extensions+message = customExtension "msg"++oldFileCreateTime :: TimeStamp -> Extensions+oldFileCreateTime = ext "oldFileCreateTime" . utcTimeBuilder++-- Max: 255 Character+oldFileHash :: T.Text -> Extensions+oldFileHash = customExtension "oldFileHash"++-- Max: 1023 Character+oldFileId :: T.Text -> Extensions+oldFileId = customExtension "oldFileId"++oldFileModificationTime :: TimeStamp -> Extensions+oldFileModificationTime = ext "oldFileModificationTime" . utcTimeBuilder++-- Max: 1023 Character+oldFileName :: T.Text -> Extensions+oldFileName = customExtension "oldFileName"++-- Max: 1023 Character+oldFilePath :: T.Text -> Extensions+oldFilePath = customExtension "oldFilePath"++-- Max: 1023 Character+oldFilePermission :: T.Text -> Extensions+oldFilePermission = customExtension "oldFilePermission"++-- Max: 1023 Character+oldFileSize :: Int -> Extensions+oldFileSize = ext "oldFileSize" . intDec++-- Max: 1023 Character+oldFileType :: T.Text -> Extensions+oldFileType = customExtension "oldFileType"++bytesOut :: Int -> Extensions+bytesOut = ext "out" . intDec+-- Max: 63 Character+eventOutcome :: T.Text -> Extensions+eventOutcome = customExtension "outcome"++transportProtocol :: T.Text -> Extensions+transportProtocol = customExtension "proto"++-- Max: 1023 Character+reason :: T.Text -> Extensions+reason = customExtension "reason"++-- Max: 1023 Character+requestURL :: T.Text -> Extensions+requestURL = customExtension "request"++-- Max: 1023 Character+requestClientApplication :: T.Text -> Extensions+requestClientApplication = customExtension "requestClientApplication"++-- Max: 1023 Character+requestCookies :: T.Text -> Extensions+requestCookies = customExtension "requestCookies"++-- Max: 1023 Character+requestMethod :: T.Text -> Extensions+requestMethod = customExtension "requestMethod"++receiptTime :: TimeStamp -> Extensions+receiptTime = ext "rt" . utcTimeBuilder++-- Max: 1023 Character+sourceHostName :: T.Text -> Extensions+sourceHostName = customExtension "shost"++sourceMacAddress :: MACAddress -> Extensions+sourceMacAddress = customExtension "smac"++-- Max: 255 Character+sourceNtDomain :: T.Text -> Extensions+sourceNtDomain = customExtension "sntdom"++-- Max: 255 Character+sourceDnsDomain :: T.Text -> Extensions+sourceDnsDomain = customExtension "sourceDnsDomain"++-- Max: 1023 Character+sourceServiceName :: T.Text -> Extensions+sourceServiceName = customExtension "sourceServiceName"++sourceTranslatedAddress :: IPv4Address -> Extensions+sourceTranslatedAddress = customExtension "sourceTranslatedAddress"++sourceTranslatedPort :: Int -> Extensions+sourceTranslatedPort = ext "sourceTranslatedPort" . intDec++sourceProcessId :: Int -> Extensions+sourceProcessId = ext "spid" . intDec++-- Max: 1023 Character+sourceUserPrivileges :: T.Text -> Extensions+sourceUserPrivileges = customExtension "spriv"++-- Max: 1023 Character+sourceProcessName :: T.Text -> Extensions+sourceProcessName = customExtension "sproc"++sourcePort :: Int -> Extensions+sourcePort = ext "spt" . intDec++sourceAddress :: IPv4Address -> Extensions+sourceAddress = customExtension "src"++startTime :: TimeStamp -> Extensions+startTime = ext "start" . utcTimeBuilder++-- Max: 1023 Character+sourceUserId :: T.Text -> Extensions+sourceUserId = customExtension "suid"++-- Max: 1023 Character+sourceUserName :: T.Text -> Extensions+sourceUserName = customExtension "suser"+++-- TODO: enforce data size limits for each extension
+ test/doctests.hs view
@@ -0,0 +1,31 @@+module Main where+--------------------------------------------------------------------------------+import Build_doctests (deps)+import Control.Applicative+import Control.Monad+import Data.List+import System.Directory+import System.FilePath+import Test.DocTest+--------------------------------------------------------------------------------+main :: IO ()+main = getSources >>= \sources -> doctest $+ "-isrc"+ : "-idist/build/autogen"+ : "-optP-include"+ : "-optPdist/build/autogen/cabal_macros.h"+ : "-hide-all-packages"+ : map ("-package="++) deps ++ sources++getSources :: IO [FilePath]+getSources = filter (isSuffixOf ".hs") <$> go "src"+ where+ go dir = do+ (dirs, files) <- getFilesAndDirectories dir+ (files ++) . concat <$> mapM go dirs++getFilesAndDirectories :: FilePath -> IO ([FilePath], [FilePath])+getFilesAndDirectories dir = do+ c <- map (dir </>) . filter (`notElem` ["..", "."]) <$> getDirectoryContents dir+ (,) <$> filterM doesDirectoryExist c <*> filterM doesFileExist c+