From 75b6643efa7ce4481e1c76edf910b9f176261517 Mon Sep 17 00:00:00 2001 From: Erik Darzins Date: Wed, 11 Jan 2023 18:11:17 +0000 Subject: [PATCH] Add Support for NAP Pre-compiled Publication Add Agent support for pre-compiled NAP content published via an external source. --- go.mod | 4 +- src/core/config/config.go | 4 +- src/core/config/defaults.go | 11 +- src/core/config/types.go | 7 +- .../nap/attack_signatures.go | 54 +++++ .../nap/attack_signatures_test.go | 70 +++++++ src/extensions/nginx-app-protect/nap/nap.go | 15 ++ .../nginx-app-protect/nap/nap_content.go | 72 +++++++ .../nginx-app-protect/nap/nap_content_test.go | 196 ++++++++++++++++++ .../nginx-app-protect/nap/nap_metadata.go | 134 ++++++++++++ .../nginx-app-protect/nap/threat_campaigns.go | 54 +++++ .../nap/threat_campaigns_test.go | 70 +++++++ src/extensions/nginx-app-protect/nap/types.go | 18 +- src/plugins/nginx.go | 60 ++++-- src/plugins/nginx_app_protect.go | 20 +- .../nginx/agent/v2/src/core/config/config.go | 4 +- .../agent/v2/src/core/config/defaults.go | 11 +- .../nginx/agent/v2/src/core/config/types.go | 7 +- .../nap/attack_signatures.go | 54 +++++ .../extensions/nginx-app-protect/nap/nap.go | 15 ++ .../nginx-app-protect/nap/nap_content.go | 72 +++++++ .../nginx-app-protect/nap/nap_metadata.go | 134 ++++++++++++ .../nginx-app-protect/nap/threat_campaigns.go | 54 +++++ .../extensions/nginx-app-protect/nap/types.go | 18 +- .../nginx/agent/v2/src/plugins/nginx.go | 60 ++++-- .../agent/v2/src/plugins/nginx_app_protect.go | 20 +- 26 files changed, 1162 insertions(+), 76 deletions(-) create mode 100644 src/extensions/nginx-app-protect/nap/attack_signatures.go create mode 100644 src/extensions/nginx-app-protect/nap/attack_signatures_test.go create mode 100644 src/extensions/nginx-app-protect/nap/nap_content.go create mode 100644 src/extensions/nginx-app-protect/nap/nap_content_test.go create mode 100644 src/extensions/nginx-app-protect/nap/nap_metadata.go create mode 100644 src/extensions/nginx-app-protect/nap/threat_campaigns.go create mode 100644 src/extensions/nginx-app-protect/nap/threat_campaigns_test.go create mode 100644 test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/attack_signatures.go create mode 100644 test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap_content.go create mode 100644 test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap_metadata.go create mode 100644 test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/threat_campaigns.go diff --git a/go.mod b/go.mod index 1ee957ded5..8885046590 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,9 @@ require ( require ( github.com/go-resty/resty/v2 v2.7.0 github.com/nginx/agent/sdk/v2 v2.0.0-00010101000000-000000000000 + github.com/nginxinc/nginx-go-crossplane v0.4.1 github.com/prometheus/client_golang v1.13.0 + gopkg.in/yaml.v2 v2.4.0 ) require ( @@ -54,7 +56,6 @@ require ( github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/nginxinc/nginx-go-crossplane v0.4.1 // indirect github.com/pascaldekloe/name v1.0.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.2 // indirect @@ -80,7 +81,6 @@ require ( google.golang.org/genproto v0.0.0-20220805133916-01dd62135a58 // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) replace github.com/nginx/agent/sdk/v2 => ./sdk diff --git a/src/core/config/config.go b/src/core/config/config.go index 6477aff6ff..363c68c6de 100644 --- a/src/core/config/config.go +++ b/src/core/config/config.go @@ -83,6 +83,7 @@ func SetDefaults() { func SetNginxAppProtectDefaults() { Viper.SetDefault(NginxAppProtectReportInterval, Defaults.NginxAppProtect.ReportInterval) + Viper.SetDefault(NginxAppProtectPrecompiledPublication, Defaults.NginxAppProtect.PrecompiledPublication) } func SetNAPMonitoringDefaults() { @@ -307,7 +308,8 @@ func getDataplane() Dataplane { func getNginxAppProtect() NginxAppProtect { return NginxAppProtect{ - ReportInterval: Viper.GetDuration(NginxAppProtectReportInterval), + ReportInterval: Viper.GetDuration(NginxAppProtectReportInterval), + PrecompiledPublication: Viper.GetBool(NginxAppProtectPrecompiledPublication), } } diff --git a/src/core/config/defaults.go b/src/core/config/defaults.go index 722ddcce44..871c3e7263 100644 --- a/src/core/config/defaults.go +++ b/src/core/config/defaults.go @@ -82,6 +82,9 @@ var ( ReportInterval: time.Minute, ReportCount: 400, }, + NginxAppProtect: NginxAppProtect{ + PrecompiledPublication: false, + }, } AllowedDirectoriesMap map[string]struct{} ) @@ -172,7 +175,8 @@ const ( // viper keys used in config NginxAppProtectKey = "nginx_app_protect" - NginxAppProtectReportInterval = NginxAppProtectKey + agent_config.KeyDelimiter + "report_interval" + NginxAppProtectReportInterval = NginxAppProtectKey + agent_config.KeyDelimiter + "report_interval" + NginxAppProtectPrecompiledPublication = NginxAppProtectKey + agent_config.KeyDelimiter + "precompiled_publication" // viper keys used in config NAPMonitoringKey = "nap_monitoring" @@ -362,6 +366,11 @@ var ( Name: NginxAppProtectReportInterval, Usage: "The period of time the agent will check for App Protect software changes on the dataplane", }, + &BoolFlag{ + Name: NginxAppProtectPrecompiledPublication, + Usage: "Enables publication of NGINX App Protect pre-compiled content from an external source.", + DefaultValue: Defaults.NginxAppProtect.PrecompiledPublication, + }, // NAP Monitoring &IntFlag{ Name: NAPMonitoringCollectorBufferSize, diff --git a/src/core/config/types.go b/src/core/config/types.go index 4ced9dcb37..4fc97cb2e9 100644 --- a/src/core/config/types.go +++ b/src/core/config/types.go @@ -46,6 +46,10 @@ func (c *Config) IsNginxAppProtectConfigured() bool { return c.NginxAppProtect != (NginxAppProtect{}) } +func (c *Config) IsNginxAppProtectPrecompiledPublicationConfigured() bool { + return c.NginxAppProtect.PrecompiledPublication +} + func (c *Config) IsFeatureEnabled(feature string) bool { for _, configFeature := range c.Features { if configFeature == feature { @@ -120,7 +124,8 @@ type AdvancedMetrics struct { } type NginxAppProtect struct { - ReportInterval time.Duration `mapstructure:"report_interval" yaml:"-"` + ReportInterval time.Duration `mapstructure:"report_interval" yaml:"-"` + PrecompiledPublication bool `mapstructure:"precompiled_publication" yaml:"-"` } type NAPMonitoring struct { diff --git a/src/extensions/nginx-app-protect/nap/attack_signatures.go b/src/extensions/nginx-app-protect/nap/attack_signatures.go new file mode 100644 index 0000000000..6a53feb6ad --- /dev/null +++ b/src/extensions/nginx-app-protect/nap/attack_signatures.go @@ -0,0 +1,54 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "fmt" + "io/ioutil" + "time" + + "github.com/nginx/agent/v2/src/core" + + "gopkg.in/yaml.v2" +) + +// getAttackSignaturesVersion gets the version of the attack signatures package that is +// installed on the system, the version format is YYYY.MM.DD. +func getAttackSignaturesVersion(versionFile string) (string, error) { + // Check if attack signatures version file exists + logger.Debugf("Checking for the required NAP attack signatures version file - %v\n", versionFile) + installed, err := core.FileExists(versionFile) + if !installed && err == nil { + return "", nil + } else if err != nil { + return "", err + } + + // Get the version bytes + versionBytes, err := ioutil.ReadFile(versionFile) + if err != nil { + return "", err + } + + // Read bytes into object + attackSigVersionDateTime := napRevisionDateTime{} + err = yaml.UnmarshalStrict([]byte(versionBytes), &attackSigVersionDateTime) + if err != nil { + return "", err + } + + // Convert revision date into the proper version format + attackSigTime, err := time.Parse(time.RFC3339, attackSigVersionDateTime.RevisionDatetime) + if err != nil { + return "", err + } + attackSignatureReleaseVersion := fmt.Sprintf("%d.%02d.%02d", attackSigTime.Year(), attackSigTime.Month(), attackSigTime.Day()) + logger.Debugf("Converted attack signature version (%s) found in %s to - %s\n", attackSigVersionDateTime.RevisionDatetime, ATTACK_SIGNATURES_UPDATE_FILE, attackSignatureReleaseVersion) + + return attackSignatureReleaseVersion, nil +} diff --git a/src/extensions/nginx-app-protect/nap/attack_signatures_test.go b/src/extensions/nginx-app-protect/nap/attack_signatures_test.go new file mode 100644 index 0000000000..3c1d28fc6a --- /dev/null +++ b/src/extensions/nginx-app-protect/nap/attack_signatures_test.go @@ -0,0 +1,70 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testAttackSigVersionFile = "/tmp/test-attack-sigs-version.yaml" + testAttackSigVersionFileContents = `--- +checksum: t+N7AHGIKPhdDwb8zMZh2w +filename: signatures.bin.tgz +revisionDatetime: 2022-02-24T20:32:01Z` +) + +func TestGetAttackSignaturesVersion(t *testing.T) { + testCases := []struct { + testName string + versionFile string + attackSigDateTime *napRevisionDateTime + expVersion string + expError error + }{ + { + testName: "AttackSignaturesInstalled", + versionFile: testAttackSigVersionFile, + attackSigDateTime: &napRevisionDateTime{ + RevisionDatetime: "2022-02-24T20:32:01Z", + }, + expVersion: "2022.02.24", + expError: nil, + }, + { + testName: "AttackSignaturesNotInstalled", + versionFile: ATTACK_SIGNATURES_UPDATE_FILE, + attackSigDateTime: nil, + expVersion: "", + expError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + // Create a fake version file if required by test + if tc.attackSigDateTime != nil { + err := os.WriteFile(tc.versionFile, []byte(testAttackSigVersionFileContents), 0644) + require.NoError(t, err) + + defer func() { + err := os.Remove(tc.versionFile) + require.NoError(t, err) + }() + } + + version, err := getAttackSignaturesVersion(tc.versionFile) + assert.Equal(t, err, tc.expError) + assert.Equal(t, tc.expVersion, version) + }) + } +} diff --git a/src/extensions/nginx-app-protect/nap/nap.go b/src/extensions/nginx-app-protect/nap/nap.go index dddb4a7708..1b7428a95a 100644 --- a/src/extensions/nginx-app-protect/nap/nap.go +++ b/src/extensions/nginx-app-protect/nap/nap.go @@ -8,6 +8,7 @@ package nap import ( + "fmt" "io/fs" "os" "path/filepath" @@ -62,8 +63,22 @@ func NewNginxAppProtect(optDirPath, symLinkDir string) (*NginxAppProtect, error) } } + // Get attack signatures version + attackSigsVersion, err := getAttackSignaturesVersion(ATTACK_SIGNATURES_UPDATE_FILE) + if err != nil && err.Error() != fmt.Sprintf(FILE_NOT_FOUND, ATTACK_SIGNATURES_UPDATE_FILE) { + return nil, err + } + + // Get threat campaigns version + threatCampaignsVersion, err := getThreatCampaignsVersion(THREAT_CAMPAIGNS_UPDATE_FILE) + if err != nil && err.Error() != fmt.Sprintf(FILE_NOT_FOUND, THREAT_CAMPAIGNS_UPDATE_FILE) { + return nil, err + } + // Update the NAP object with the values from NAP on the system nap.Status = status.String() + nap.AttackSignaturesVersion = attackSigsVersion + nap.ThreatCampaignsVersion = threatCampaignsVersion if napRelease != nil { nap.Release = *napRelease } diff --git a/src/extensions/nginx-app-protect/nap/nap_content.go b/src/extensions/nginx-app-protect/nap/nap_content.go new file mode 100644 index 0000000000..5802179ccf --- /dev/null +++ b/src/extensions/nginx-app-protect/nap/nap_content.go @@ -0,0 +1,72 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "path" + + "github.com/nginx/agent/sdk/v2" + "github.com/nginx/agent/sdk/v2/proto" + + "github.com/nginxinc/nginx-go-crossplane" +) + +// getContent parses the config for NAP policies and profiles +func getContent(cfg *proto.NginxConfig) ([]string, []string) { + policyMap := make(map[string]bool) + profileMap := make(map[string]bool) + + for _, directory := range cfg.GetDirectoryMap().GetDirectories() { + for _, file := range directory.GetFiles() { + confFile := path.Join(directory.GetName(), file.GetName()) + payload, err := crossplane.Parse(confFile, + &crossplane.ParseOptions{ + SingleFile: false, + StopParsingOnError: true, + }, + ) + if err != nil { + continue + } + for _, conf := range payload.Config { + err = sdk.CrossplaneConfigTraverse(&conf, + func(parent *crossplane.Directive, directive *crossplane.Directive) (bool, error) { + switch directive.Directive { + case "app_protect_policy_file": + if len(directive.Args) == 1 { + _, policy := path.Split(directive.Args[0]) + policyMap[policy] = true + } + case "app_protect_security_log": + if len(directive.Args) == 2 { + _, profile := path.Split(directive.Args[0]) + profileMap[profile] = true + } + } + return true, nil + }) + if err != nil { + continue + } + } + if err != nil { + continue + } + } + } + policies := []string{} + for policy, _ := range policyMap { + policies = append(policies, policy) + } + profiles := []string{} + for profile, _ := range profileMap { + profiles = append(profiles, profile) + } + + return policies, profiles +} diff --git a/src/extensions/nginx-app-protect/nap/nap_content_test.go b/src/extensions/nginx-app-protect/nap/nap_content_test.go new file mode 100644 index 0000000000..705f5bfef5 --- /dev/null +++ b/src/extensions/nginx-app-protect/nap/nap_content_test.go @@ -0,0 +1,196 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/nginx/agent/sdk/v2" + + "github.com/stretchr/testify/assert" +) + +const ( + nginxID = "1" + systemID = "2" +) + +var config0 = `daemon off; + worker_processes 2; + user www-data; + + events { + use epoll; + worker_connections 128; + } + + error_log /tmp/testdata/logs/error.log info; + + http { + log_format upstream_time '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + 'rt=$request_time uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time"'; + + server_tokens off; + charset utf-8; + + access_log /tmp/testdata/logs/access1.log $upstream_time; + + server { + server_name localhost; + listen 127.0.0.1:80; + + error_page 500 502 503 504 /50x.html; + # ssl_certificate /usr/local/nginx/conf/cert.pem; + + location / { + root /tmp/testdata/root; + } + + location /privateapi { + limit_except GET { + auth_basic "NGINX Plus API"; + auth_basic_user_file /path/to/passwd/file; + } + api write=on; + allow 127.0.0.1; + deny all; + } + } + + access_log /tmp/testdata/logs/access2.log combined; + + }` + +var config1 = `daemon off; + worker_processes 2; + user www-data; + + events { + use epoll; + worker_connections 128; + } + + error_log /tmp/testdata/logs/error.log info; + + http { + app_protect_enable on; + app_protect_security_log_enable on; + + log_format upstream_time '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + 'rt=$request_time uct="$upstream_connect_time" uht="$upstream_header_time" urt="$upstream_response_time"'; + + server_tokens off; + charset utf-8; + + access_log /tmp/testdata/logs/access1.log $upstream_time; + app_protect_policy_file /tmp/testdata/root/my-nap-policy1.json; + app_protect_security_log "/tmp/testdata/root/log-all.json" /var/log/ssecurity.log; + + server { + server_name localhost; + listen 127.0.0.1:80; + app_protect_policy_file /tmp/testdata/root/my-nap-policy2.json; + app_protect_security_log "/tmp/testdata/root/log-blocked.json" /var/log/ssecurity.log; + + error_page 500 502 503 504 /50x.html; + # ssl_certificate /usr/local/nginx/conf/cert.pem; + + location / { + root /tmp/testdata/root; + app_protect_policy_file /tmp/testdata/root/my-nap-policy3.json; + app_protect_security_log "/tmp/testdata/root/log-default.json" /var/log/security.log; + } + + location /home { + app_protect_policy_file /tmp/testdata/root/my-nap-policy4.json; + app_protect_security_log "/tmp/testdata/root/log-illegal.json" /var/log/security.log; + } + + location /privateapi { + app_protect_policy_file /tmp/testdata/root/my-nap-policy4.json; + app_protect_security_log "/tmp/testdata/root/log-illegal.json" /var/log/security.log; + limit_except GET { + auth_basic "NGINX Plus API"; + auth_basic_user_file /path/to/passwd/file; + } + api write=on; + allow 127.0.0.1; + deny all; + } + } + + access_log /tmp/testdata/logs/access2.log combined; + + }` + +func TestNAPContent(t *testing.T) { + testCases := []struct { + testName string + file string + config string + expPolicies []string + expProfiles []string + }{ + { + testName: "NoNAPContent", + file: "/tmp/testdata/nginx/nginx.conf", + config: config0, + expPolicies: []string{}, + expProfiles: []string{}, + }, + { + testName: "ConfigWithNAPContent", + file: "/tmp/testdata/nginx/nginx2.conf", + config: config1, + expPolicies: []string{"my-nap-policy2.json", "my-nap-policy1.json", "my-nap-policy3.json", "my-nap-policy4.json"}, + expProfiles: []string{"log-all.json", "log-blocked.json", "log-default.json", "log-illegal.json"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + defer tearDownDirectories() + + err := setUpFile(tc.file, []byte(tc.config)) + assert.NoError(t, err) + + allowedDirs := map[string]struct{}{} + + cfg, err := sdk.GetNginxConfig(tc.file, nginxID, systemID, allowedDirs) + assert.NoError(t, err) + + policies, profiles := getContent(cfg) + assert.ElementsMatch(t, tc.expPolicies, policies) + assert.ElementsMatch(t, tc.expProfiles, profiles) + }) + } +} + +func setUpFile(file string, content []byte) error { + err := os.MkdirAll(filepath.Dir(file), 0755) + if err != nil { + return err + } + err = ioutil.WriteFile(file, content, 0644) + if err != nil { + return err + } + + return nil +} + +func tearDownDirectories() { + os.RemoveAll("/tmp/testdata") +} diff --git a/src/extensions/nginx-app-protect/nap/nap_metadata.go b/src/extensions/nginx-app-protect/nap/nap_metadata.go new file mode 100644 index 0000000000..8ef6165852 --- /dev/null +++ b/src/extensions/nginx-app-protect/nap/nap_metadata.go @@ -0,0 +1,134 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "encoding/json" + "os" + + "github.com/nginx/agent/sdk/v2/proto" + + log "github.com/sirupsen/logrus" +) + +// UpdateMetadata retrieves the NAP content from the config and writes +// the metadata +func UpdateMetadata( + cfg *proto.NginxConfig, + currentPrecompiledPublication bool, + wafLocation, + wafVersion, + wafAttackSignaturesVersion, + wafThreatCampaignsVersion string, +) error { + // Read NAP metadata + data, err := os.ReadFile(wafLocation) + if err != nil { + return err + } + + var oldMeta Metadata + if err := json.Unmarshal(data, &oldMeta); err != nil { + return err + } + + // Write the metadata if precomp publication is false, or + // when precomp publication toggles to true. + // If toggled, write metadata once more then the publisher + // will send metadata thereafter. + if oldMeta.PrecompiledPublication && currentPrecompiledPublication { + return nil + } + + policies, profiles := getContent(cfg) + + policyBundles := []*BundleMetadata{} + profileBundles := []*BundleMetadata{} + + for _, policy := range policies { + bundle := &BundleMetadata{ + Name: policy, + } + policyBundles = append(policyBundles, bundle) + } + for _, profile := range profiles { + bundle := &BundleMetadata{ + Name: profile, + } + profileBundles = append(profileBundles, bundle) + } + + metadata := &Metadata{ + NapVersion: wafVersion, + PrecompiledPublication: currentPrecompiledPublication, + AttackSignatureRevisionTimestamp: wafAttackSignaturesVersion, + ThreatCampaignRevisionTimestamp: wafThreatCampaignsVersion, + Policies: policyBundles, + Profiles: profileBundles, + } + + // Check if metadata changed, don't need to write if unchanged + if metadataAreEqual(&oldMeta, metadata) { + return nil + } + + m, err := json.Marshal(metadata) + if err != nil { + return err + } + log.Debugf("Writing NAP Metadata %s", m) + + return os.WriteFile(wafLocation, m, 0644) +} + +// metadataAreEqual compares the metadata for equality +func metadataAreEqual(oldMeta, newMeta *Metadata) bool { + if oldMeta.NapVersion != newMeta.NapVersion { + return false + } + if oldMeta.PrecompiledPublication != newMeta.PrecompiledPublication { + return false + } + if oldMeta.AttackSignatureRevisionTimestamp != newMeta.AttackSignatureRevisionTimestamp { + return false + } + if oldMeta.ThreatCampaignRevisionTimestamp != newMeta.ThreatCampaignRevisionTimestamp { + return false + } + if len(oldMeta.Policies) != len(newMeta.Policies) { + return false + } + if len(oldMeta.Profiles) != len(newMeta.Profiles) { + return false + } + for _, oldPolicy := range oldMeta.Policies { + found := false + for _, newPolicy := range newMeta.Policies { + if newPolicy.Name == oldPolicy.Name { + found = true + break + } + } + if !found { + return false + } + } + for _, oldProfile := range oldMeta.Profiles { + found := false + for _, newProfile := range newMeta.Profiles { + if newProfile.Name == oldProfile.Name { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/src/extensions/nginx-app-protect/nap/threat_campaigns.go b/src/extensions/nginx-app-protect/nap/threat_campaigns.go new file mode 100644 index 0000000000..1d123760ee --- /dev/null +++ b/src/extensions/nginx-app-protect/nap/threat_campaigns.go @@ -0,0 +1,54 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "fmt" + "io/ioutil" + "time" + + "github.com/nginx/agent/v2/src/core" + + "gopkg.in/yaml.v2" +) + +// getThreatCampaignsVersion gets the version of the Threat campaigns package that is +// installed on the system, the version format is YYYY.MM.DD. +func getThreatCampaignsVersion(versionFile string) (string, error) { + // Check if attack signatures version file exists + logger.Debugf("Checking for the required NAP threat campaigns version file - %v\n", versionFile) + installed, err := core.FileExists(versionFile) + if !installed && err == nil { + return "", nil + } else if err != nil { + return "", err + } + + // Get the version bytes + versionBytes, err := ioutil.ReadFile(versionFile) + if err != nil { + return "", err + } + + // Read bytes into object + threatCampVersionDateTime := napRevisionDateTime{} + err = yaml.UnmarshalStrict([]byte(versionBytes), &threatCampVersionDateTime) + if err != nil { + return "", err + } + + // Convert revision date into the proper version format + threatCampTime, err := time.Parse(time.RFC3339, threatCampVersionDateTime.RevisionDatetime) + if err != nil { + return "", err + } + threatCampaignsReleaseVersion := fmt.Sprintf("%d.%02d.%02d", threatCampTime.Year(), threatCampTime.Month(), threatCampTime.Day()) + logger.Debugf("Converted threat campaigns version (%s) found in %s to - %s\n", threatCampVersionDateTime.RevisionDatetime, THREAT_CAMPAIGNS_UPDATE_FILE, threatCampaignsReleaseVersion) + + return threatCampaignsReleaseVersion, nil +} diff --git a/src/extensions/nginx-app-protect/nap/threat_campaigns_test.go b/src/extensions/nginx-app-protect/nap/threat_campaigns_test.go new file mode 100644 index 0000000000..32f131c868 --- /dev/null +++ b/src/extensions/nginx-app-protect/nap/threat_campaigns_test.go @@ -0,0 +1,70 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testThreatCampaignsVersionFile = "/tmp/test-threat-campaigns-version.yaml" + testThreatCampaignsVersionFileContents = `--- +checksum: ALCdgk8CQgQQLRJ1ydZA4g +filename: threat_campaigns.bin.tgz +revisionDatetime: 2022-03-01T20:32:01Z` +) + +func TestGetThreatCampaignsVersion(t *testing.T) { + testCases := []struct { + testName string + versionFile string + threatCampaignDateTime *napRevisionDateTime + expVersion string + expError error + }{ + { + testName: "ThreatCampaignsInstalled", + versionFile: testThreatCampaignsVersionFile, + threatCampaignDateTime: &napRevisionDateTime{ + RevisionDatetime: "2022-03-01T20:32:01Z", + }, + expVersion: "2022.03.01", + expError: nil, + }, + { + testName: "ThreatCampaignsNotInstalled", + versionFile: THREAT_CAMPAIGNS_UPDATE_FILE, + threatCampaignDateTime: nil, + expVersion: "", + expError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + // Create a fake version file if required by test + if tc.threatCampaignDateTime != nil { + err := os.WriteFile(tc.versionFile, []byte(testThreatCampaignsVersionFileContents), 0644) + require.NoError(t, err) + + defer func() { + err := os.Remove(tc.versionFile) + require.NoError(t, err) + }() + } + + version, err := getThreatCampaignsVersion(tc.versionFile) + assert.Equal(t, err, tc.expError) + assert.Equal(t, tc.expVersion, version) + }) + } +} diff --git a/src/extensions/nginx-app-protect/nap/types.go b/src/extensions/nginx-app-protect/nap/types.go index 158560eff2..2bbd943689 100644 --- a/src/extensions/nginx-app-protect/nap/types.go +++ b/src/extensions/nginx-app-protect/nap/types.go @@ -79,10 +79,20 @@ type NAPReleaseMap struct { ReleaseMap map[string]NAPRelease `json:"releases"` } +// napRevisionDateTime is an object used to get the version for attack signatures and +// threat campaigns, as their versions are the same as their revision dates which can be +// captured in their yaml files under the field "revisionDatetime". +type napRevisionDateTime struct { + RevisionDatetime string `yaml:"revisionDatetime,omitempty"` + Checksum string `yaml:"checksum,omitempty"` + Filename string `yaml:"filename,omitempty"` +} + type Metadata struct { NapVersion string `json:"napVersion"` - GlobalStateFileName string `json:"globalStateFileName"` - GlobalStateFileUID string `json:"globalStateFileUID"` + PrecompiledPublication bool `json:"precompiledPublication"` + GlobalStateFileName string `json:"globalStateFileName,omitempty"` + GlobalStateFileUID string `json:"globalStateFileUID,omitempty"` AttackSignatureRevisionTimestamp string `json:"attackSignatureRevisionTimestamp,omitempty"` AttackSignatureUID string `json:"attackSignatureUID,omitempty"` ThreatCampaignRevisionTimestamp string `json:"threatCampaignRevisionTimestamp,omitempty"` @@ -93,6 +103,6 @@ type Metadata struct { type BundleMetadata struct { Name string `json:"name"` - UID string `json:"uid"` - RevisionTimestamp int64 `json:"revisionTimestamp"` + UID string `json:"uid,omitempty"` + RevisionTimestamp int64 `json:"revisionTimestamp,omitempty"` } diff --git a/src/plugins/nginx.go b/src/plugins/nginx.go index 019ef24aa8..a336b1729b 100644 --- a/src/plugins/nginx.go +++ b/src/plugins/nginx.go @@ -42,17 +42,20 @@ var ( // Nginx is the metadata of our nginx binary type Nginx struct { - messagePipeline core.MessagePipeInterface - nginxBinary core.NginxBinary - processes []core.Process - env core.Environment - cmdr client.Commander - config *config.Config - isNAPEnabled bool - isFeatureNginxConfigEnabled bool - configApplyStatusChannel chan *proto.Command_NginxConfigResponse - wafVersion string - wafLocation string + messagePipeline core.MessagePipeInterface + nginxBinary core.NginxBinary + processes []core.Process + env core.Environment + cmdr client.Commander + config *config.Config + isNAPEnabled bool + isNAPPrecompiledPublicationEnabled bool + isFeatureNginxConfigEnabled bool + configApplyStatusChannel chan *proto.Command_NginxConfigResponse + wafVersion string + wafLocation string + wafAttackSignaturesVersion string + wafThreatCampaignsVersion string } type ConfigRollbackResponse struct { @@ -79,19 +82,19 @@ type NginxConfigValidationResponse struct { } func NewNginx(cmdr client.Commander, nginxBinary core.NginxBinary, env core.Environment, loadedConfig *config.Config) *Nginx { - isNAPEnabled := loadedConfig.IsNginxAppProtectConfigured() isFeatureNginxConfigEnabled := loadedConfig.IsFeatureEnabled(agent_config.FeatureNginxConfig) return &Nginx{ - nginxBinary: nginxBinary, - processes: env.Processes(), - env: env, - cmdr: cmdr, - config: loadedConfig, - isNAPEnabled: isNAPEnabled, - isFeatureNginxConfigEnabled: isFeatureNginxConfigEnabled, - configApplyStatusChannel: make(chan *proto.Command_NginxConfigResponse, 1), - wafLocation: nap.APP_PROTECT_METADATA_FILE_PATH, + nginxBinary: nginxBinary, + processes: env.Processes(), + env: env, + cmdr: cmdr, + config: loadedConfig, + isNAPEnabled: loadedConfig.IsNginxAppProtectConfigured(), + isNAPPrecompiledPublicationEnabled: loadedConfig.IsNginxAppProtectPrecompiledPublicationConfigured(), + isFeatureNginxConfigEnabled: isFeatureNginxConfigEnabled, + configApplyStatusChannel: make(chan *proto.Command_NginxConfigResponse, 1), + wafLocation: nap.APP_PROTECT_METADATA_FILE_PATH, } } @@ -217,6 +220,17 @@ func (n *Nginx) uploadConfig(config *proto.ConfigDescriptor, messageId string) e } if n.isNAPEnabled { + err = nap.UpdateMetadata( + cfg, + n.isNAPPrecompiledPublicationEnabled, + n.wafLocation, + n.wafVersion, + n.wafAttackSignaturesVersion, + n.wafThreatCampaignsVersion, + ) + if err != nil { + log.Errorf("Unable to update NAP metadata: %v", err) + } cfg, err = sdk.AddAuxfileToNginxConfig(nginx.GetConfPath(), cfg, n.wafLocation, n.config.AllowedDirectoriesMap, true) if err != nil { log.Errorf("Unable to add aux file %s to nginx config: %v", n.wafLocation, err) @@ -236,6 +250,8 @@ func (n *Nginx) processDataplaneSoftwareDetails(details *proto.DataplaneSoftware log.Tracef("software details updated software %+v", details) n.wafVersion = details.AppProtectWafDetails.WafVersion + n.wafAttackSignaturesVersion = details.AppProtectWafDetails.AttackSignaturesVersion + n.wafThreatCampaignsVersion = details.AppProtectWafDetails.ThreatCampaignsVersion } func (n *Nginx) processCmd(cmd *proto.Command) { @@ -634,7 +650,9 @@ func (n *Nginx) syncAgentConfigChange() { if conf.NginxAppProtect != (config.NginxAppProtect{}) { n.isNAPEnabled = true + n.isNAPPrecompiledPublicationEnabled = conf.IsNginxAppProtectPrecompiledPublicationConfigured() } else { + n.isNAPPrecompiledPublicationEnabled = false n.isNAPEnabled = false } diff --git a/src/plugins/nginx_app_protect.go b/src/plugins/nginx_app_protect.go index 29db807bfb..481022b51b 100644 --- a/src/plugins/nginx_app_protect.go +++ b/src/plugins/nginx_app_protect.go @@ -31,12 +31,13 @@ const ( // NginxAppProtect monitors the NAP installation on the system and reports back its details type NginxAppProtect struct { - nap nap.NginxAppProtect - messagePipeline core.MessagePipeInterface - env core.Environment - reportInterval time.Duration - ctx context.Context - ctxCancel context.CancelFunc + nap nap.NginxAppProtect + messagePipeline core.MessagePipeInterface + env core.Environment + reportInterval time.Duration + precompiledPublication bool + ctx context.Context + ctxCancel context.CancelFunc } func NewNginxAppProtect(config *config.Config, env core.Environment) (*NginxAppProtect, error) { @@ -55,9 +56,10 @@ func NewNginxAppProtect(config *config.Config, env core.Environment) (*NginxAppP } nginxAppProtect := &NginxAppProtect{ - nap: *napTime, - env: env, - reportInterval: reportInterval, + nap: *napTime, + env: env, + reportInterval: reportInterval, + precompiledPublication: config.NginxAppProtect.PrecompiledPublication, } return nginxAppProtect, nil diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/config.go b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/config.go index 6477aff6ff..363c68c6de 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/config.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/config.go @@ -83,6 +83,7 @@ func SetDefaults() { func SetNginxAppProtectDefaults() { Viper.SetDefault(NginxAppProtectReportInterval, Defaults.NginxAppProtect.ReportInterval) + Viper.SetDefault(NginxAppProtectPrecompiledPublication, Defaults.NginxAppProtect.PrecompiledPublication) } func SetNAPMonitoringDefaults() { @@ -307,7 +308,8 @@ func getDataplane() Dataplane { func getNginxAppProtect() NginxAppProtect { return NginxAppProtect{ - ReportInterval: Viper.GetDuration(NginxAppProtectReportInterval), + ReportInterval: Viper.GetDuration(NginxAppProtectReportInterval), + PrecompiledPublication: Viper.GetBool(NginxAppProtectPrecompiledPublication), } } diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/defaults.go b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/defaults.go index 722ddcce44..871c3e7263 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/defaults.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/defaults.go @@ -82,6 +82,9 @@ var ( ReportInterval: time.Minute, ReportCount: 400, }, + NginxAppProtect: NginxAppProtect{ + PrecompiledPublication: false, + }, } AllowedDirectoriesMap map[string]struct{} ) @@ -172,7 +175,8 @@ const ( // viper keys used in config NginxAppProtectKey = "nginx_app_protect" - NginxAppProtectReportInterval = NginxAppProtectKey + agent_config.KeyDelimiter + "report_interval" + NginxAppProtectReportInterval = NginxAppProtectKey + agent_config.KeyDelimiter + "report_interval" + NginxAppProtectPrecompiledPublication = NginxAppProtectKey + agent_config.KeyDelimiter + "precompiled_publication" // viper keys used in config NAPMonitoringKey = "nap_monitoring" @@ -362,6 +366,11 @@ var ( Name: NginxAppProtectReportInterval, Usage: "The period of time the agent will check for App Protect software changes on the dataplane", }, + &BoolFlag{ + Name: NginxAppProtectPrecompiledPublication, + Usage: "Enables publication of NGINX App Protect pre-compiled content from an external source.", + DefaultValue: Defaults.NginxAppProtect.PrecompiledPublication, + }, // NAP Monitoring &IntFlag{ Name: NAPMonitoringCollectorBufferSize, diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/types.go b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/types.go index 4ced9dcb37..4fc97cb2e9 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/types.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/types.go @@ -46,6 +46,10 @@ func (c *Config) IsNginxAppProtectConfigured() bool { return c.NginxAppProtect != (NginxAppProtect{}) } +func (c *Config) IsNginxAppProtectPrecompiledPublicationConfigured() bool { + return c.NginxAppProtect.PrecompiledPublication +} + func (c *Config) IsFeatureEnabled(feature string) bool { for _, configFeature := range c.Features { if configFeature == feature { @@ -120,7 +124,8 @@ type AdvancedMetrics struct { } type NginxAppProtect struct { - ReportInterval time.Duration `mapstructure:"report_interval" yaml:"-"` + ReportInterval time.Duration `mapstructure:"report_interval" yaml:"-"` + PrecompiledPublication bool `mapstructure:"precompiled_publication" yaml:"-"` } type NAPMonitoring struct { diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/attack_signatures.go b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/attack_signatures.go new file mode 100644 index 0000000000..6a53feb6ad --- /dev/null +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/attack_signatures.go @@ -0,0 +1,54 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "fmt" + "io/ioutil" + "time" + + "github.com/nginx/agent/v2/src/core" + + "gopkg.in/yaml.v2" +) + +// getAttackSignaturesVersion gets the version of the attack signatures package that is +// installed on the system, the version format is YYYY.MM.DD. +func getAttackSignaturesVersion(versionFile string) (string, error) { + // Check if attack signatures version file exists + logger.Debugf("Checking for the required NAP attack signatures version file - %v\n", versionFile) + installed, err := core.FileExists(versionFile) + if !installed && err == nil { + return "", nil + } else if err != nil { + return "", err + } + + // Get the version bytes + versionBytes, err := ioutil.ReadFile(versionFile) + if err != nil { + return "", err + } + + // Read bytes into object + attackSigVersionDateTime := napRevisionDateTime{} + err = yaml.UnmarshalStrict([]byte(versionBytes), &attackSigVersionDateTime) + if err != nil { + return "", err + } + + // Convert revision date into the proper version format + attackSigTime, err := time.Parse(time.RFC3339, attackSigVersionDateTime.RevisionDatetime) + if err != nil { + return "", err + } + attackSignatureReleaseVersion := fmt.Sprintf("%d.%02d.%02d", attackSigTime.Year(), attackSigTime.Month(), attackSigTime.Day()) + logger.Debugf("Converted attack signature version (%s) found in %s to - %s\n", attackSigVersionDateTime.RevisionDatetime, ATTACK_SIGNATURES_UPDATE_FILE, attackSignatureReleaseVersion) + + return attackSignatureReleaseVersion, nil +} diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap.go b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap.go index dddb4a7708..1b7428a95a 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap.go @@ -8,6 +8,7 @@ package nap import ( + "fmt" "io/fs" "os" "path/filepath" @@ -62,8 +63,22 @@ func NewNginxAppProtect(optDirPath, symLinkDir string) (*NginxAppProtect, error) } } + // Get attack signatures version + attackSigsVersion, err := getAttackSignaturesVersion(ATTACK_SIGNATURES_UPDATE_FILE) + if err != nil && err.Error() != fmt.Sprintf(FILE_NOT_FOUND, ATTACK_SIGNATURES_UPDATE_FILE) { + return nil, err + } + + // Get threat campaigns version + threatCampaignsVersion, err := getThreatCampaignsVersion(THREAT_CAMPAIGNS_UPDATE_FILE) + if err != nil && err.Error() != fmt.Sprintf(FILE_NOT_FOUND, THREAT_CAMPAIGNS_UPDATE_FILE) { + return nil, err + } + // Update the NAP object with the values from NAP on the system nap.Status = status.String() + nap.AttackSignaturesVersion = attackSigsVersion + nap.ThreatCampaignsVersion = threatCampaignsVersion if napRelease != nil { nap.Release = *napRelease } diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap_content.go b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap_content.go new file mode 100644 index 0000000000..5802179ccf --- /dev/null +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap_content.go @@ -0,0 +1,72 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "path" + + "github.com/nginx/agent/sdk/v2" + "github.com/nginx/agent/sdk/v2/proto" + + "github.com/nginxinc/nginx-go-crossplane" +) + +// getContent parses the config for NAP policies and profiles +func getContent(cfg *proto.NginxConfig) ([]string, []string) { + policyMap := make(map[string]bool) + profileMap := make(map[string]bool) + + for _, directory := range cfg.GetDirectoryMap().GetDirectories() { + for _, file := range directory.GetFiles() { + confFile := path.Join(directory.GetName(), file.GetName()) + payload, err := crossplane.Parse(confFile, + &crossplane.ParseOptions{ + SingleFile: false, + StopParsingOnError: true, + }, + ) + if err != nil { + continue + } + for _, conf := range payload.Config { + err = sdk.CrossplaneConfigTraverse(&conf, + func(parent *crossplane.Directive, directive *crossplane.Directive) (bool, error) { + switch directive.Directive { + case "app_protect_policy_file": + if len(directive.Args) == 1 { + _, policy := path.Split(directive.Args[0]) + policyMap[policy] = true + } + case "app_protect_security_log": + if len(directive.Args) == 2 { + _, profile := path.Split(directive.Args[0]) + profileMap[profile] = true + } + } + return true, nil + }) + if err != nil { + continue + } + } + if err != nil { + continue + } + } + } + policies := []string{} + for policy, _ := range policyMap { + policies = append(policies, policy) + } + profiles := []string{} + for profile, _ := range profileMap { + profiles = append(profiles, profile) + } + + return policies, profiles +} diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap_metadata.go b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap_metadata.go new file mode 100644 index 0000000000..8ef6165852 --- /dev/null +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/nap_metadata.go @@ -0,0 +1,134 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "encoding/json" + "os" + + "github.com/nginx/agent/sdk/v2/proto" + + log "github.com/sirupsen/logrus" +) + +// UpdateMetadata retrieves the NAP content from the config and writes +// the metadata +func UpdateMetadata( + cfg *proto.NginxConfig, + currentPrecompiledPublication bool, + wafLocation, + wafVersion, + wafAttackSignaturesVersion, + wafThreatCampaignsVersion string, +) error { + // Read NAP metadata + data, err := os.ReadFile(wafLocation) + if err != nil { + return err + } + + var oldMeta Metadata + if err := json.Unmarshal(data, &oldMeta); err != nil { + return err + } + + // Write the metadata if precomp publication is false, or + // when precomp publication toggles to true. + // If toggled, write metadata once more then the publisher + // will send metadata thereafter. + if oldMeta.PrecompiledPublication && currentPrecompiledPublication { + return nil + } + + policies, profiles := getContent(cfg) + + policyBundles := []*BundleMetadata{} + profileBundles := []*BundleMetadata{} + + for _, policy := range policies { + bundle := &BundleMetadata{ + Name: policy, + } + policyBundles = append(policyBundles, bundle) + } + for _, profile := range profiles { + bundle := &BundleMetadata{ + Name: profile, + } + profileBundles = append(profileBundles, bundle) + } + + metadata := &Metadata{ + NapVersion: wafVersion, + PrecompiledPublication: currentPrecompiledPublication, + AttackSignatureRevisionTimestamp: wafAttackSignaturesVersion, + ThreatCampaignRevisionTimestamp: wafThreatCampaignsVersion, + Policies: policyBundles, + Profiles: profileBundles, + } + + // Check if metadata changed, don't need to write if unchanged + if metadataAreEqual(&oldMeta, metadata) { + return nil + } + + m, err := json.Marshal(metadata) + if err != nil { + return err + } + log.Debugf("Writing NAP Metadata %s", m) + + return os.WriteFile(wafLocation, m, 0644) +} + +// metadataAreEqual compares the metadata for equality +func metadataAreEqual(oldMeta, newMeta *Metadata) bool { + if oldMeta.NapVersion != newMeta.NapVersion { + return false + } + if oldMeta.PrecompiledPublication != newMeta.PrecompiledPublication { + return false + } + if oldMeta.AttackSignatureRevisionTimestamp != newMeta.AttackSignatureRevisionTimestamp { + return false + } + if oldMeta.ThreatCampaignRevisionTimestamp != newMeta.ThreatCampaignRevisionTimestamp { + return false + } + if len(oldMeta.Policies) != len(newMeta.Policies) { + return false + } + if len(oldMeta.Profiles) != len(newMeta.Profiles) { + return false + } + for _, oldPolicy := range oldMeta.Policies { + found := false + for _, newPolicy := range newMeta.Policies { + if newPolicy.Name == oldPolicy.Name { + found = true + break + } + } + if !found { + return false + } + } + for _, oldProfile := range oldMeta.Profiles { + found := false + for _, newProfile := range newMeta.Profiles { + if newProfile.Name == oldProfile.Name { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/threat_campaigns.go b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/threat_campaigns.go new file mode 100644 index 0000000000..1d123760ee --- /dev/null +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/threat_campaigns.go @@ -0,0 +1,54 @@ +/** + * Copyright (c) F5, Inc. + * + * This source code is licensed under the Apache License, Version 2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +package nap + +import ( + "fmt" + "io/ioutil" + "time" + + "github.com/nginx/agent/v2/src/core" + + "gopkg.in/yaml.v2" +) + +// getThreatCampaignsVersion gets the version of the Threat campaigns package that is +// installed on the system, the version format is YYYY.MM.DD. +func getThreatCampaignsVersion(versionFile string) (string, error) { + // Check if attack signatures version file exists + logger.Debugf("Checking for the required NAP threat campaigns version file - %v\n", versionFile) + installed, err := core.FileExists(versionFile) + if !installed && err == nil { + return "", nil + } else if err != nil { + return "", err + } + + // Get the version bytes + versionBytes, err := ioutil.ReadFile(versionFile) + if err != nil { + return "", err + } + + // Read bytes into object + threatCampVersionDateTime := napRevisionDateTime{} + err = yaml.UnmarshalStrict([]byte(versionBytes), &threatCampVersionDateTime) + if err != nil { + return "", err + } + + // Convert revision date into the proper version format + threatCampTime, err := time.Parse(time.RFC3339, threatCampVersionDateTime.RevisionDatetime) + if err != nil { + return "", err + } + threatCampaignsReleaseVersion := fmt.Sprintf("%d.%02d.%02d", threatCampTime.Year(), threatCampTime.Month(), threatCampTime.Day()) + logger.Debugf("Converted threat campaigns version (%s) found in %s to - %s\n", threatCampVersionDateTime.RevisionDatetime, THREAT_CAMPAIGNS_UPDATE_FILE, threatCampaignsReleaseVersion) + + return threatCampaignsReleaseVersion, nil +} diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/types.go b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/types.go index 158560eff2..2bbd943689 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/types.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/extensions/nginx-app-protect/nap/types.go @@ -79,10 +79,20 @@ type NAPReleaseMap struct { ReleaseMap map[string]NAPRelease `json:"releases"` } +// napRevisionDateTime is an object used to get the version for attack signatures and +// threat campaigns, as their versions are the same as their revision dates which can be +// captured in their yaml files under the field "revisionDatetime". +type napRevisionDateTime struct { + RevisionDatetime string `yaml:"revisionDatetime,omitempty"` + Checksum string `yaml:"checksum,omitempty"` + Filename string `yaml:"filename,omitempty"` +} + type Metadata struct { NapVersion string `json:"napVersion"` - GlobalStateFileName string `json:"globalStateFileName"` - GlobalStateFileUID string `json:"globalStateFileUID"` + PrecompiledPublication bool `json:"precompiledPublication"` + GlobalStateFileName string `json:"globalStateFileName,omitempty"` + GlobalStateFileUID string `json:"globalStateFileUID,omitempty"` AttackSignatureRevisionTimestamp string `json:"attackSignatureRevisionTimestamp,omitempty"` AttackSignatureUID string `json:"attackSignatureUID,omitempty"` ThreatCampaignRevisionTimestamp string `json:"threatCampaignRevisionTimestamp,omitempty"` @@ -93,6 +103,6 @@ type Metadata struct { type BundleMetadata struct { Name string `json:"name"` - UID string `json:"uid"` - RevisionTimestamp int64 `json:"revisionTimestamp"` + UID string `json:"uid,omitempty"` + RevisionTimestamp int64 `json:"revisionTimestamp,omitempty"` } diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/nginx.go b/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/nginx.go index 019ef24aa8..a336b1729b 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/nginx.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/nginx.go @@ -42,17 +42,20 @@ var ( // Nginx is the metadata of our nginx binary type Nginx struct { - messagePipeline core.MessagePipeInterface - nginxBinary core.NginxBinary - processes []core.Process - env core.Environment - cmdr client.Commander - config *config.Config - isNAPEnabled bool - isFeatureNginxConfigEnabled bool - configApplyStatusChannel chan *proto.Command_NginxConfigResponse - wafVersion string - wafLocation string + messagePipeline core.MessagePipeInterface + nginxBinary core.NginxBinary + processes []core.Process + env core.Environment + cmdr client.Commander + config *config.Config + isNAPEnabled bool + isNAPPrecompiledPublicationEnabled bool + isFeatureNginxConfigEnabled bool + configApplyStatusChannel chan *proto.Command_NginxConfigResponse + wafVersion string + wafLocation string + wafAttackSignaturesVersion string + wafThreatCampaignsVersion string } type ConfigRollbackResponse struct { @@ -79,19 +82,19 @@ type NginxConfigValidationResponse struct { } func NewNginx(cmdr client.Commander, nginxBinary core.NginxBinary, env core.Environment, loadedConfig *config.Config) *Nginx { - isNAPEnabled := loadedConfig.IsNginxAppProtectConfigured() isFeatureNginxConfigEnabled := loadedConfig.IsFeatureEnabled(agent_config.FeatureNginxConfig) return &Nginx{ - nginxBinary: nginxBinary, - processes: env.Processes(), - env: env, - cmdr: cmdr, - config: loadedConfig, - isNAPEnabled: isNAPEnabled, - isFeatureNginxConfigEnabled: isFeatureNginxConfigEnabled, - configApplyStatusChannel: make(chan *proto.Command_NginxConfigResponse, 1), - wafLocation: nap.APP_PROTECT_METADATA_FILE_PATH, + nginxBinary: nginxBinary, + processes: env.Processes(), + env: env, + cmdr: cmdr, + config: loadedConfig, + isNAPEnabled: loadedConfig.IsNginxAppProtectConfigured(), + isNAPPrecompiledPublicationEnabled: loadedConfig.IsNginxAppProtectPrecompiledPublicationConfigured(), + isFeatureNginxConfigEnabled: isFeatureNginxConfigEnabled, + configApplyStatusChannel: make(chan *proto.Command_NginxConfigResponse, 1), + wafLocation: nap.APP_PROTECT_METADATA_FILE_PATH, } } @@ -217,6 +220,17 @@ func (n *Nginx) uploadConfig(config *proto.ConfigDescriptor, messageId string) e } if n.isNAPEnabled { + err = nap.UpdateMetadata( + cfg, + n.isNAPPrecompiledPublicationEnabled, + n.wafLocation, + n.wafVersion, + n.wafAttackSignaturesVersion, + n.wafThreatCampaignsVersion, + ) + if err != nil { + log.Errorf("Unable to update NAP metadata: %v", err) + } cfg, err = sdk.AddAuxfileToNginxConfig(nginx.GetConfPath(), cfg, n.wafLocation, n.config.AllowedDirectoriesMap, true) if err != nil { log.Errorf("Unable to add aux file %s to nginx config: %v", n.wafLocation, err) @@ -236,6 +250,8 @@ func (n *Nginx) processDataplaneSoftwareDetails(details *proto.DataplaneSoftware log.Tracef("software details updated software %+v", details) n.wafVersion = details.AppProtectWafDetails.WafVersion + n.wafAttackSignaturesVersion = details.AppProtectWafDetails.AttackSignaturesVersion + n.wafThreatCampaignsVersion = details.AppProtectWafDetails.ThreatCampaignsVersion } func (n *Nginx) processCmd(cmd *proto.Command) { @@ -634,7 +650,9 @@ func (n *Nginx) syncAgentConfigChange() { if conf.NginxAppProtect != (config.NginxAppProtect{}) { n.isNAPEnabled = true + n.isNAPPrecompiledPublicationEnabled = conf.IsNginxAppProtectPrecompiledPublicationConfigured() } else { + n.isNAPPrecompiledPublicationEnabled = false n.isNAPEnabled = false } diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/nginx_app_protect.go b/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/nginx_app_protect.go index 29db807bfb..481022b51b 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/nginx_app_protect.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/nginx_app_protect.go @@ -31,12 +31,13 @@ const ( // NginxAppProtect monitors the NAP installation on the system and reports back its details type NginxAppProtect struct { - nap nap.NginxAppProtect - messagePipeline core.MessagePipeInterface - env core.Environment - reportInterval time.Duration - ctx context.Context - ctxCancel context.CancelFunc + nap nap.NginxAppProtect + messagePipeline core.MessagePipeInterface + env core.Environment + reportInterval time.Duration + precompiledPublication bool + ctx context.Context + ctxCancel context.CancelFunc } func NewNginxAppProtect(config *config.Config, env core.Environment) (*NginxAppProtect, error) { @@ -55,9 +56,10 @@ func NewNginxAppProtect(config *config.Config, env core.Environment) (*NginxAppP } nginxAppProtect := &NginxAppProtect{ - nap: *napTime, - env: env, - reportInterval: reportInterval, + nap: *napTime, + env: env, + reportInterval: reportInterval, + precompiledPublication: config.NginxAppProtect.PrecompiledPublication, } return nginxAppProtect, nil