Skip to content

Commit e0c1fa3

Browse files
authored
Add bot signature version to AppProtectWAFDetails (#1240)
1 parent fca0312 commit e0c1fa3

File tree

18 files changed

+488
-107
lines changed

18 files changed

+488
-107
lines changed

docs/proto/proto.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,7 @@ Represents App Protect WAF details
12781278
| waf_location | [string](#string) | | Location of WAF metadata file |
12791279
| precompiled_publication | [bool](#bool) | | Determines whether the publication of NGINX App Protect pre-compiled content from an external source is supported |
12801280
| waf_release | [string](#string) | | WAF release |
1281+
| bot_signatures_version | [string](#string) | | Bot Signatures version |
12811282

12821283

12831284

sdk/proto/nap.pb.go

Lines changed: 88 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/proto/nap.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ message AppProtectWAFDetails {
2121
bool precompiled_publication = 6 [(gogoproto.jsontag) = "precompiled_publication"];
2222
// WAF release
2323
string waf_release = 7 [(gogoproto.jsontag) = "waf_release"];
24+
// Bot Signatures version
25+
string bot_signatures_version = 8 [(gogoproto.jsontag) = "bot_signatures_version"];
2426
}
2527

2628
// Represents the health of App Protect WAF
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright (c) F5, Inc.
3+
*
4+
* This source code is licensed under the Apache License, Version 2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package nap
9+
10+
import (
11+
"fmt"
12+
"os"
13+
"time"
14+
15+
"github.com/nginx/agent/v2/src/core"
16+
17+
"gopkg.in/yaml.v2"
18+
)
19+
20+
// botSignaturesVersion gets the version of the bot signatures package that is
21+
// installed on the system, the version format is YYYY.MM.DD.
22+
func botSignaturesVersion(versionFile string) (string, error) {
23+
// Check if bot signatures version file exists
24+
logger.Debugf("Checking for the required NAP bot signatures version file - %v\n", versionFile)
25+
installed, err := core.FileExists(versionFile)
26+
if !installed && err == nil {
27+
return "", nil
28+
} else if err != nil {
29+
return "", err
30+
}
31+
32+
// Get the version bytes
33+
versionBytes, err := os.ReadFile(versionFile)
34+
if err != nil {
35+
return "", err
36+
}
37+
38+
// Read bytes into object
39+
botSigVersionDateTime := napRevisionDateTime{}
40+
err = yaml.Unmarshal([]byte(versionBytes), &botSigVersionDateTime)
41+
if err != nil {
42+
return "", err
43+
}
44+
45+
// Convert revision date into the proper version format
46+
botSigTime, err := time.Parse(time.RFC3339, botSigVersionDateTime.RevisionDatetime)
47+
if err != nil {
48+
return "", err
49+
}
50+
botSignatureReleaseVersion := fmt.Sprintf("%d.%02d.%02d", botSigTime.Year(), botSigTime.Month(), botSigTime.Day())
51+
logger.Debugf("Converted bot signature version (%s) found in %s to - %s\n", botSigVersionDateTime.RevisionDatetime, BOT_SIGNATURES_UPDATE_FILE, botSignatureReleaseVersion)
52+
53+
return botSignatureReleaseVersion, nil
54+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Copyright (c) F5, Inc.
3+
*
4+
* This source code is licensed under the Apache License, Version 2.0 license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package nap
9+
10+
import (
11+
"os"
12+
"testing"
13+
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
const (
19+
testBotSigVersionFile = "/tmp/test-bot-sigs-version.yaml"
20+
testBotSigVersionFileContents = `---
21+
checksum: nmlUse0aQzwLvuAaqDW/Jw
22+
filename: bot_signatures.bin.tgz
23+
revisionDatetime: 2025-08-21T11:30:33Z
24+
`
25+
)
26+
27+
func Test_BotSignaturesVersion(t *testing.T) {
28+
testCases := []struct {
29+
testName string
30+
versionFile string
31+
botSigDateTime *napRevisionDateTime
32+
expVersion string
33+
expError error
34+
}{
35+
{
36+
testName: "BotSignaturesInstalled",
37+
versionFile: testBotSigVersionFile,
38+
botSigDateTime: &napRevisionDateTime{
39+
RevisionDatetime: "2025-08-21T11:30:33Z",
40+
},
41+
expVersion: "2025.08.21",
42+
expError: nil,
43+
},
44+
{
45+
testName: "BotSignaturesNotInstalled",
46+
versionFile: BOT_SIGNATURES_UPDATE_FILE,
47+
botSigDateTime: nil,
48+
expVersion: "",
49+
expError: nil,
50+
},
51+
}
52+
53+
for _, tc := range testCases {
54+
t.Run(tc.testName, func(t *testing.T) {
55+
// Create a fake version file if required by test
56+
if tc.botSigDateTime != nil {
57+
err := os.WriteFile(tc.versionFile, []byte(testBotSigVersionFileContents), 0o644)
58+
require.NoError(t, err)
59+
60+
defer func() {
61+
err := os.Remove(tc.versionFile)
62+
require.NoError(t, err)
63+
}()
64+
}
65+
66+
version, err := botSignaturesVersion(tc.versionFile)
67+
assert.Equal(t, err, tc.expError)
68+
assert.Equal(t, tc.expVersion, version)
69+
})
70+
}
71+
}

src/extensions/nginx-app-protect/nap/const.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const (
2525
// THREAT_CAMPAIGN_VERSION_FILE = "/opt/app_protect/var/update_files/threat_campaigns/version"
2626
ATTACK_SIGNATURES_UPDATE_FILE = "/opt/app_protect/var/update_files/signatures/signature_update.yaml"
2727
THREAT_CAMPAIGNS_UPDATE_FILE = "/opt/app_protect/var/update_files/threat_campaigns/threat_campaign_update.yaml"
28+
BOT_SIGNATURES_UPDATE_FILE = "/opt/app_protect/var/update_files/bot_signatures/bot_signature_update.yaml"
2829

2930
APP_PROTECT_METADATA_FILE_PATH = "/etc/nms/app_protect_metadata.json"
3031
)

src/extensions/nginx-app-protect/nap/nap.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func NewNginxAppProtect(optDirPath, symLinkDir string) (*NginxAppProtect, error)
4646
Release: NAPRelease{},
4747
AttackSignaturesVersion: "",
4848
ThreatCampaignsVersion: "",
49+
BotSignaturesVersion: "",
4950
optDirPath: optDirPath,
5051
symLinkDir: symLinkDir,
5152
}
@@ -77,10 +78,18 @@ func NewNginxAppProtect(optDirPath, symLinkDir string) (*NginxAppProtect, error)
7778
return nil, err
7879
}
7980

81+
// Get bot signatures version
82+
botSigsVersion, err := botSignaturesVersion(BOT_SIGNATURES_UPDATE_FILE)
83+
if err != nil && err.Error() != fmt.Sprintf(FILE_NOT_FOUND, BOT_SIGNATURES_UPDATE_FILE) {
84+
return nil, err
85+
}
86+
8087
// Update the NAP object with the values from NAP on the system
8188
nap.Status = status.String()
8289
nap.AttackSignaturesVersion = attackSigsVersion
8390
nap.ThreatCampaignsVersion = threatCampaignsVersion
91+
nap.BotSignaturesVersion = botSigsVersion
92+
8493
if napRelease != nil {
8594
nap.Release = *napRelease
8695
}
@@ -156,6 +165,7 @@ func (nap *NginxAppProtect) monitor(ctx context.Context, msgChannel chan NAPRepo
156165
nap.Release = newNap.Release
157166
nap.AttackSignaturesVersion = newNap.AttackSignaturesVersion
158167
nap.ThreatCampaignsVersion = newNap.ThreatCampaignsVersion
168+
nap.BotSignaturesVersion = newNap.BotSignaturesVersion
159169
mu.Unlock()
160170

161171
// Send the update message through the channel
@@ -255,6 +265,7 @@ func (nap *NginxAppProtect) GenerateNAPReport() NAPReport {
255265
Status: nap.Status,
256266
AttackSignaturesVersion: nap.AttackSignaturesVersion,
257267
ThreatCampaignsVersion: nap.ThreatCampaignsVersion,
268+
BotSignaturesVersion: nap.BotSignaturesVersion,
258269
}
259270
}
260271

@@ -265,7 +276,8 @@ func (nap *NginxAppProtect) napReportIsEqual(incomingNAPReport NAPReport) bool {
265276
return (currentNAPReport.NAPVersion == incomingNAPReport.NAPVersion) &&
266277
(currentNAPReport.Status == incomingNAPReport.Status) &&
267278
(currentNAPReport.AttackSignaturesVersion == incomingNAPReport.AttackSignaturesVersion) &&
268-
(currentNAPReport.ThreatCampaignsVersion == incomingNAPReport.ThreatCampaignsVersion)
279+
(currentNAPReport.ThreatCampaignsVersion == incomingNAPReport.ThreatCampaignsVersion) &&
280+
(currentNAPReport.BotSignaturesVersion == incomingNAPReport.BotSignaturesVersion)
269281
}
270282

271283
// napInstalled determines if NAP is installed on the system. If NAP is NOT installed on the

src/extensions/nginx-app-protect/nap/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type NginxAppProtect struct {
1717
Release NAPRelease
1818
AttackSignaturesVersion string
1919
ThreatCampaignsVersion string
20+
BotSignaturesVersion string
2021
optDirPath string
2122
symLinkDir string
2223
}
@@ -28,6 +29,7 @@ type NAPReport struct {
2829
NAPRelease string
2930
AttackSignaturesVersion string
3031
ThreatCampaignsVersion string
32+
BotSignaturesVersion string
3133
}
3234

3335
// NAPReportBundle is meant to capture the NAPReport before an update
@@ -100,6 +102,8 @@ type Metadata struct {
100102
AttackSignatureUID string `json:"attackSignatureUID,omitempty"`
101103
ThreatCampaignRevisionTimestamp string `json:"threatCampaignRevisionTimestamp,omitempty"`
102104
ThreatCampaignUID string `json:"threatCampaignUID,omitempty"`
105+
BotSignatureRevisionTimestamp string `json:"botSignatureRevisionTimestamp,omitempty"`
106+
BotSignatureUID string `json:"botSignatureUID,omitempty"`
103107
Policies []*BundleMetadata `json:"policyMetadata,omitempty"`
104108
Profiles []*BundleMetadata `json:"logProfileMetadata,omitempty"`
105109
}

0 commit comments

Comments
 (0)