2022-08-05 22:07:32 +00:00
package spec
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
2025-09-19 15:57:02 +00:00
"github.com/fleetdm/fleet/v4/pkg/testutils"
2024-05-09 15:29:16 +00:00
"github.com/hashicorp/go-multierror"
2022-08-05 22:07:32 +00:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSplitYaml ( t * testing . T ) {
in := `
-- -
- Document
# -- -
-- - Document2
-- -
Document3
`
docs := SplitYaml ( in )
require . Equal ( t , 3 , len ( docs ) )
assert . Equal ( t , "- Document\n#---" , docs [ 0 ] )
assert . Equal ( t , "Document2" , docs [ 1 ] )
assert . Equal ( t , "Document3" , docs [ 2 ] )
}
func gitRootPath ( t * testing . T ) string {
path , err := exec . Command ( "git" , "rev-parse" , "--show-toplevel" ) . Output ( )
require . NoError ( t , err )
return strings . TrimSpace ( string ( path ) )
}
2023-01-27 17:58:00 +00:00
func loadSpec ( t * testing . T , relativePaths ... string ) [ ] byte {
2022-08-05 22:07:32 +00:00
b , err := os . ReadFile ( filepath . Join (
2023-01-27 17:58:00 +00:00
append ( [ ] string { gitRootPath ( t ) } , relativePaths ... ) ... ,
2022-08-05 22:07:32 +00:00
) )
require . NoError ( t , err )
return b
}
2023-01-27 17:58:00 +00:00
func TestGroupFromBytesWithStdLib ( t * testing . T ) {
stdQueryLib := loadSpec ( t ,
"docs" , "01-Using-Fleet" , "standard-query-library" , "standard-query-library.yml" ,
)
2022-08-05 22:07:32 +00:00
g , err := GroupFromBytes ( stdQueryLib )
require . NoError ( t , err )
require . NotEmpty ( t , g . Queries )
require . NotEmpty ( t , g . Policies )
}
2023-01-27 17:58:00 +00:00
func TestGroupFromBytesWithMacOS13CISQueries ( t * testing . T ) {
cisQueries := loadSpec ( t ,
"ee" , "cis" , "macos-13" , "cis-policy-queries.yml" ,
)
g , err := GroupFromBytes ( cisQueries )
require . NoError ( t , err )
require . NotEmpty ( t , g . Policies )
}
2023-03-03 19:11:04 +00:00
func TestGroupFromBytesWithWin10CISQueries ( t * testing . T ) {
cisQueries := loadSpec ( t ,
"ee" , "cis" , "win-10" , "cis-policy-queries.yml" ,
)
g , err := GroupFromBytes ( cisQueries )
require . NoError ( t , err )
require . NotEmpty ( t , g . Policies )
}
2023-11-22 13:48:28 +00:00
func TestGroupFromBytesMissingFields ( t * testing . T ) {
tests := [ ] struct {
name string
in [ ] byte
want string
} {
{
"missing spec" ,
[ ] byte ( `
-- -
apiVersion : v1
kind : team
` ) ,
` Missing required fields ("spec") on provided "team" configuration. ` ,
} ,
{
"missing spec and kind" ,
[ ] byte ( `
-- -
apiVersion : v1
` ) ,
` Missing required fields ("spec", "kind") on provided configuration ` ,
} ,
{
"missing spec and empty string kind" ,
[ ] byte ( `
-- -
apiVersion : v1
kind : ""
` ) ,
` Missing required fields ("spec", "kind") on provided configuration ` ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
_ , err := GroupFromBytes ( tt . in )
require . ErrorContains ( t , err , tt . want )
} )
}
}
2024-05-09 15:29:16 +00:00
func TestEscapeString ( t * testing . T ) {
for _ , tc := range [ ] struct {
s string
expResult string
} {
{ ` $foo ` , ` $foo ` } , // nothing to escape
{ ` bar$foo ` , ` bar$foo ` } , // nothing to escape
{ ` bar$ { foo} ` , ` bar$ { foo} ` } , // nothing to escape
{ ` \$foo ` , ` $PREVENT_ESCAPING_foo ` } , // escaping
{ ` bar\$foo ` , ` bar$PREVENT_ESCAPING_foo ` } , // escaping
{ ` \\$foo ` , ` \\$foo ` } , // no escaping
{ ` bar\\$foo ` , ` bar\\$foo ` } , // no escaping
{ ` \\\$foo ` , ` \$PREVENT_ESCAPING_foo ` } , // escaping
{ ` bar\\\$foo ` , ` bar\$PREVENT_ESCAPING_foo ` } , // escaping
{ ` bar\\\$ { foo}bar ` , ` bar\$PREVENT_ESCAPING_ { foo}bar ` } , // escaping
{ ` \\\\$foo ` , ` \\\\$foo ` } , // no escaping
{ ` bar\\\\$foo ` , ` bar\\\\$foo ` } , // no escaping
{ ` bar\\\\$ { foo} ` , ` bar\\\\$ { foo} ` } , // no escaping
} {
result := escapeString ( tc . s , "PREVENT_ESCAPING_" )
require . Equal ( t , tc . expResult , result )
}
}
2024-12-11 21:05:48 +00:00
func checkMultiErrors ( t * testing . T , errs ... string ) func ( err error ) {
return func ( err error ) {
me , ok := err . ( * multierror . Error )
require . True ( t , ok )
require . Len ( t , me . Errors , len ( errs ) )
for i , err := range me . Errors {
require . Equal ( t , errs [ i ] , err . Error ( ) )
2024-05-09 15:29:16 +00:00
}
}
2024-12-11 21:05:48 +00:00
}
2024-05-09 15:29:16 +00:00
2024-12-11 21:05:48 +00:00
func TestExpandEnv ( t * testing . T ) {
2024-05-09 15:29:16 +00:00
for _ , tc := range [ ] struct {
environment map [ string ] string
s string
expResult string
checkErr func ( error )
} {
{ map [ string ] string { "foo" : "1" } , ` $foo ` , ` 1 ` , nil } ,
2024-10-16 16:12:48 +00:00
{ map [ string ] string { "foo" : "1" } , ` $foo $FLEET_VAR_BAR $ { FLEET_VAR_BAR}x $ { foo} ` , ` 1 $FLEET_VAR_BAR $ { FLEET_VAR_BAR}x 1 ` , nil } ,
2024-05-09 15:29:16 +00:00
{ map [ string ] string { "foo" : "" } , ` $foo ` , ` ` , nil } ,
{ map [ string ] string { "foo" : "" , "bar" : "" , "zoo" : "" } , ` $foo$ { bar}$zoo ` , ` ` , nil } ,
2024-12-11 21:05:48 +00:00
{ map [ string ] string { } , ` $foo ` , ` ` , checkMultiErrors ( t , "environment variable \"foo\" not set" ) } ,
{ map [ string ] string { "foo" : "1" } , ` $foo$bar ` , ` ` , checkMultiErrors ( t , "environment variable \"bar\" not set" ) } ,
2025-09-19 15:57:02 +00:00
{
map [ string ] string { "bar" : "1" } ,
` $foo $bar $zoo ` , ` ` ,
checkMultiErrors ( t , "environment variable \"foo\" not set" , "environment variable \"zoo\" not set" ) ,
} ,
2024-05-09 15:29:16 +00:00
{ map [ string ] string { "foo" : "4" , "bar" : "2" } , ` $foo$bar ` , ` 42 ` , nil } ,
{ map [ string ] string { "foo" : "42" , "bar" : "" } , ` $foo$bar ` , ` 42 ` , nil } ,
2024-12-11 21:05:48 +00:00
{ map [ string ] string { } , ` $$ ` , ` ` , checkMultiErrors ( t , "environment variable \"$\" not set" ) } ,
{ map [ string ] string { "foo" : "1" } , ` $$foo ` , ` ` , checkMultiErrors ( t , "environment variable \"$\" not set" ) } ,
2024-05-09 15:29:16 +00:00
{ map [ string ] string { "foo" : "1" } , ` \$$ { foo} ` , ` $1 ` , nil } ,
{ map [ string ] string { } , ` \$foo ` , ` $foo ` , nil } , // escaped
{ map [ string ] string { "foo" : "1" } , ` \\$foo ` , ` \\1 ` , nil } , // not escaped
{ map [ string ] string { } , ` \\\$foo ` , ` \$foo ` , nil } , // escaped
{ map [ string ] string { } , ` \\\$foo$ ` , ` \$foo$ ` , nil } , // escaped
{ map [ string ] string { } , ` bar\\\$foo$ ` , ` bar\$foo$ ` , nil } , // escaped
{ map [ string ] string { "foo" : "1" } , ` $foo var ` , ` 1 var ` , nil } , // not escaped
{ map [ string ] string { "foo" : "1" } , ` $ { foo}var ` , ` 1var ` , nil } , // not escaped
{ map [ string ] string { "foo" : "1" } , ` \$ { foo}var ` , ` $ { foo}var ` , nil } , // escaped
{ map [ string ] string { "foo" : "" } , ` $ { foo}var ` , ` var ` , nil } ,
{ map [ string ] string { "foo" : "" , "$" : "2" } , ` $ { $}$ { foo}var ` , ` 2var ` , nil } ,
2024-12-11 21:05:48 +00:00
{ map [ string ] string { } , ` $ { foo}var ` , ` ` , checkMultiErrors ( t , "environment variable \"foo\" not set" ) } ,
2024-10-16 16:12:48 +00:00
{ map [ string ] string { } , ` foo PREVENT_ESCAPING_bar $ FLEET_VAR_ ` , ` foo PREVENT_ESCAPING_bar $ FLEET_VAR_ ` , nil } , // nothing to replace
2025-09-19 15:57:02 +00:00
{
map [ string ] string { "foo" : "BAR" } ,
` \$FLEET_VAR_$foo \$ { FLEET_VAR_$foo} \$ { FLEET_VAR_$ { foo}2} ` ,
` $FLEET_VAR_BAR $ { FLEET_VAR_BAR} $ { FLEET_VAR_BAR2} ` , nil ,
} , // nested variables
2025-11-06 18:30:55 +00:00
{
map [ string ] string { } ,
"$fleet_var_test" , // Somehow, variables can be lowercased when coming in from time to time
"$fleet_var_test" , // Should not be replaced
nil ,
} ,
2026-01-07 16:58:57 +00:00
{
map [ string ] string { "custom_secret" : "test&123" } ,
"<Add>$custom_secret</Add>" ,
"<Add>test&123</Add>" ,
nil ,
} ,
2024-05-09 15:29:16 +00:00
} {
2025-09-19 15:57:02 +00:00
// save the current env before clearing it.
testutils . SaveEnv ( t )
2024-05-09 15:29:16 +00:00
os . Clearenv ( )
for k , v := range tc . environment {
2024-10-16 16:12:48 +00:00
_ = os . Setenv ( k , v )
2024-05-09 15:29:16 +00:00
}
result , err := ExpandEnv ( tc . s )
if tc . checkErr == nil {
require . NoError ( t , err )
} else {
tc . checkErr ( err )
}
require . Equal ( t , tc . expResult , result )
}
}
2024-12-11 21:05:48 +00:00
func TestLookupEnvSecrets ( t * testing . T ) {
for _ , tc := range [ ] struct {
environment map [ string ] string
s string
expResult map [ string ] string
checkErr func ( error )
} {
{ map [ string ] string { "foo" : "1" } , ` $foo ` , map [ string ] string { } , nil } ,
{ map [ string ] string { "FLEET_SECRET_foo" : "1" } , ` $FLEET_SECRET_foo ` , map [ string ] string { "FLEET_SECRET_foo" : "1" } , nil } ,
2025-09-19 15:57:02 +00:00
{
map [ string ] string { "foo" : "1" } ,
` $FLEET_SECRET_foo ` ,
map [ string ] string { } ,
checkMultiErrors ( t , "environment variable \"FLEET_SECRET_foo\" not set" ) ,
} ,
2025-12-16 13:51:48 +00:00
{ map [ string ] string { "FLEET_SECRET_foo" : "test&123" } , ` <Add>$FLEET_SECRET_foo</Add> ` , map [ string ] string { "FLEET_SECRET_foo" : "test&123" } , nil } ,
2024-12-11 21:05:48 +00:00
} {
2025-09-19 15:57:02 +00:00
// save the current env before clearing it.
testutils . SaveEnv ( t )
2024-12-11 21:05:48 +00:00
os . Clearenv ( )
for k , v := range tc . environment {
_ = os . Setenv ( k , v )
}
secretsMap := make ( map [ string ] string )
err := LookupEnvSecrets ( tc . s , secretsMap )
if tc . checkErr == nil {
require . NoError ( t , err )
} else {
tc . checkErr ( err )
}
require . Equal ( t , tc . expResult , secretsMap )
}
}
2025-04-29 19:09:25 +00:00
2025-08-22 14:37:12 +00:00
// TestExpandEnvBytesIncludingSecrets tests that FLEET_SECRET_ variables are expanded when using ExpandEnvBytesIncludingSecrets
func TestExpandEnvBytesIncludingSecrets ( t * testing . T ) {
t . Setenv ( "FLEET_SECRET_API_KEY" , "secret123" )
t . Setenv ( "NORMAL_VAR" , "normalvalue" )
t . Setenv ( "FLEET_VAR_HOST" , "hostname" )
2025-12-16 13:51:48 +00:00
t . Setenv ( "FLEET_SECRET_XML" , "secret&123" )
2025-08-22 14:37:12 +00:00
input := [ ] byte ( ` API Key : $ FLEET_SECRET_API_KEY
Normal : $ NORMAL_VAR
Fleet Var : $ FLEET_VAR_HOST
Missing : $ FLEET_SECRET_MISSING ` )
result , err := ExpandEnvBytesIncludingSecrets ( input )
require . NoError ( t , err )
expected := ` API Key : secret123
Normal : normalvalue
Fleet Var : $ FLEET_VAR_HOST
Missing : $ FLEET_SECRET_MISSING `
assert . Equal ( t , expected , string ( result ) )
// Verify that FLEET_VAR_ is not expanded (reserved for server)
assert . Contains ( t , string ( result ) , "$FLEET_VAR_HOST" )
// Verify that FLEET_SECRET_ is expanded
assert . Contains ( t , string ( result ) , "secret123" )
assert . NotContains ( t , string ( result ) , "$FLEET_SECRET_API_KEY" )
// Verify that missing secrets are left as-is
assert . Contains ( t , string ( result ) , "$FLEET_SECRET_MISSING" )
2025-12-16 13:51:48 +00:00
xmlInput := [ ] byte ( ` <Add>$FLEET_SECRET_XML</Add> ` )
xmlResult , err := ExpandEnvBytesIncludingSecrets ( xmlInput )
require . NoError ( t , err )
expectedXML := ` <Add>secret&123</Add> `
assert . Equal ( t , expectedXML , string ( xmlResult ) )
2025-08-22 14:37:12 +00:00
}
2025-04-29 19:09:25 +00:00
func TestGetExclusionZones ( t * testing . T ) {
testCases := [ ] struct {
fixturePath [ ] string
expected map [ [ 2 ] int ] string
} {
{
[ ] string { "testdata" , "policies" , "policies.yml" } ,
map [ [ 2 ] int ] string {
2025-09-19 15:57:02 +00:00
{ 46 , 106 } : " description: This policy should always fail.\n resolution:" ,
{ 93 , 155 } : " resolution: There is no resolution for this policy.\n query:" ,
{ 268 , 328 } : " description: This policy should always pass.\n resolution:" ,
{ 315 , 678 } : " resolution: |\n Automated method:\n Ask your system administrator to deploy the following script which will ensure proper Security Auditing Retention:\n cp /etc/security/audit_control ./tmp.txt; origExpire=$(cat ./tmp.txt | grep expire-after); sed \"s/${origExpire}/expire-after:60d OR 5G/\" ./tmp.txt > /etc/security/audit_control; rm ./tmp.txt;\n query:" ,
2025-04-29 19:09:25 +00:00
} ,
} ,
{
[ ] string { "testdata" , "global_config_no_paths.yml" } ,
map [ [ 2 ] int ] string {
2025-11-18 22:32:13 +00:00
{ 911 , 994 } : " description: Collect osquery performance stats directly from osquery\n query:" , //
{ 1799 , 1863 } : " description: This policy should always fail.\n resolution:" , //
{ 1848 , 1914 } : " resolution: There is no resolution for this policy.\n query:" , //
{ 2031 , 2095 } : " description: This policy should always pass.\n resolution:" , //
{ 2080 , 2146 } : " resolution: There is no resolution for this policy.\n query:" , //
{ 2439 , 2503 } : " description: This policy should always fail.\n resolution:" , //
{ 2488 , 2554 } : " resolution: There is no resolution for this policy.\n query:" , //
{ 2658 , 2722 } : " description: This policy should always fail.\n resolution:" , //
{ 2707 , 3080 } : " resolution: |\n Automated method:\n Ask your system administrator to deploy the following script which will ensure proper Security Auditing Retention:\n cp /etc/security/audit_control ./tmp.txt; origExpire=$(cat ./tmp.txt | grep expire-after); sed \"s/${origExpire}/expire-after:60d OR 5G/\" ./tmp.txt > /etc/security/audit_control; rm ./tmp.txt;\n query:" ,
{ 6147 , 6194 } : " description: A cool global label\n query:" , //
{ 6291 , 6337 } : " description: A fly global label\n hosts:" , //
2025-04-29 19:09:25 +00:00
} ,
} ,
}
for _ , tC := range testCases {
fPath := filepath . Join ( tC . fixturePath ... )
t . Run ( fPath , func ( t * testing . T ) {
fContents , err := os . ReadFile ( fPath )
require . NoError ( t , err )
contents := string ( fContents )
actual := getExclusionZones ( contents )
require . Equal ( t , len ( tC . expected ) , len ( actual ) )
for pos , text := range tC . expected {
2025-11-18 22:32:13 +00:00
assert . Contains ( t , actual , pos )
assert . Equal ( t , contents [ pos [ 0 ] : pos [ 1 ] ] , text , pos )
2025-04-29 19:09:25 +00:00
}
} )
}
}