Skip to content

Commit 1f9bd1d

Browse files
committed
Added the option of copying the audit log files to the S3 bucket.
1 parent e75726c commit 1f9bd1d

File tree

4 files changed

+85
-37
lines changed

4 files changed

+85
-37
lines changed

Monitoring/ingest_nas_audit_logs_into_cloudwatch/README-MANUAL.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ or by following the manual instructions found in the this file.
1414

1515
## Prerequisites
1616
- An FSx for Data ONTAP file system.
17-
- An S3 bucket to store the "stats" file and a Lambda layer zip file.
18-
- You will need to download the [Lambda layer zip file](https://raw.githubusercontent.com/NetApp/FSx-ONTAP-samples-scripts/main/Monitoring/ingest_nas_audit_logs_into_cloudwatch/lambda_layer.zip) from this repo and upload it to the S3 bucket. Be sure to perserve the name `lambda_layer.zip`.
19-
- The "stats" file is maintained by the program. It is used to keep track of the last time the Lambda function successfully ingested audit logs from each SVM. Its size will be small (i.e. less than a few megabytes).
17+
- An S3 bucket to store the "stats" file and optionally a copy of all the raw NAS audit log files. It will also
18+
hold a Lambda layer file needed to be able to an add Lambda Layer from a CloudFormation script.
19+
- You will need to download the [Lambda layer zip file](https://raw.githubusercontent.com/NetApp/FSx-ONTAP-samples-scripts/main/Monitoring/ingest_nas_audit_logs_into_cloudwatch/lambda_layer.zip)
20+
from this repo and upload it to the S3 bucket. Be sure to perserve the name `lambda_layer.zip`.
21+
- The "stats" file is maintained by the program. It is used to keep track of the last time the Lambda function
22+
successfully ingested audit logs from each SVM. Its size will be small (i.e. less than a few megabytes).
2023
- A CloudWatch log group to ingest the audit logs into. Each audit log file with get its own log stream within the log group.
2124
- Have NAS auditing configured and enabled on the SVM within a FSx for Data ONTAP file system. **Ensure you have selected the XML format for the audit logs.** Also,
2225
ensure you have set up a rotation schedule. The program will only act on audit log files that have been finalized, and not the "active" one. You can read this
@@ -79,7 +82,7 @@ and `DeleteNetworkInterface` actions. The correct resource line is `arn:aws:ec2:
7982
`zip -r ingest_nas_audit_logs.zip .`<br>
8083

8184
2. Within the AWS console, or using the AWS API, create a Lambda function with:
82-
1. Python 3.10, or higher, as the runtime.
85+
1. Python 3.11, or higher, as the runtime.
8386
1. Set the permissions to the role created above.
8487
1. Under `Additional Configurations` select `Enable VPC` and select a VPC and Subnet that will have access to all the FSx for ONTAP
8588
file system management endpoints that you want to gather audit logs from. Also, select a Security Group that allows TCP port 443 outbound.
@@ -96,6 +99,7 @@ process a lot of audit entries and/or process a lot of SVMs.
9699
| secretArn | The ARN of the secret that contains the credentials for all the FSx for ONTAP file systems you want to gather audit logs from. |
97100
| s3BucketRegion | The region of the S3 bucket where the stats file is stored. |
98101
| s3BucketName | The name of the S3 bucket where the stats file is stored. |
102+
| copyToS3 | Set to `true` if you want to copy the raw audit log files to the S3 bucket.|
99103
| statsName | The name you want to use as the stats file. |
100104
| logGroupName | The name of the CloudWatch log group to ingest the audit logs into. |
101105
| volumeName | The name of the volume, on all the FSx for ONTAP file systems, where the audit logs are stored. |
@@ -122,4 +126,4 @@ Unless required by applicable law or agreed to in writing, software distributed
122126

123127
See the License for the specific language governing permissions and limitations under the License.
124128

125-
© 2024 NetApp, Inc. All Rights Reserved.
129+
© 2025 NetApp, Inc. All Rights Reserved.

Monitoring/ingest_nas_audit_logs_into_cloudwatch/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ or by following the manual instructions found in the [README-MANUEL.md](README-M
1515

1616
## Prerequisites
1717
- An FSx for Data ONTAP file system.
18-
- An S3 bucket to store the "stats" file and a Lambda layer zip file.
19-
- You will need to download the [Lambda layer zip file](https://raw.githubusercontent.com/NetApp/FSx-ONTAP-samples-scripts/main/Monitoring/ingest_nas_audit_logs_into_cloudwatch/lambda_layer.zip) from this repo and upload it to the S3 bucket. Be sure to preserve the name `lambda_layer.zip`.
20-
- The "stats" file is maintained by the program. It is used to keep track of the last time the Lambda function successfully ingested audit logs from each SVM. Its size will be small (i.e. less than a few megabytes).
18+
- An S3 bucket to store the "stats" file and optionally a copy of all the raw NAS audit log files. It will also
19+
hold a Lambda layer file needed to be able to an add Lambda Layer from a CloudFormation script.
20+
- You will need to download the [Lambda layer zip file](https://raw.githubusercontent.com/NetApp/FSx-ONTAP-samples-scripts/main/Monitoring/ingest_nas_audit_logs_into_cloudwatch/lambda_layer.zip)
21+
from this repo and upload it to the S3 bucket. Be sure to preserve the name `lambda_layer.zip`.
22+
- The "stats" file is maintained by the program. It is used to keep track of the last time the Lambda function successfully
23+
ingested audit logs from each SVM. Its size will be small (i.e. less than a few megabytes).
2124
- A CloudWatch log group to ingest the audit logs into. Each audit log file will get its own log stream within the log group.
2225
- Have NAS auditing configured and enabled on the SVM within a FSx for Data ONTAP file system. **Ensure you have selected the XML format for the audit logs.** Also,
2326
ensure you have set up a rotation schedule. The program will only act on audit log files that have been finalized, and not the "active" one. You can read this
@@ -92,11 +95,12 @@ and `DeleteNetworkInterface` actions. The correct resource line is `arn:aws:ec2:
9295
|lambdaSecruityGroupsIds|Yes|Select the security groups that you want the Lambda function associated with. The security group must allow outbound traffic on TCP port 443. Inbound rules don't matter since the Lambda function is not accessible from a network.|
9396
|s3BucketName|Yes|The name of the S3 bucket where the stats file is stored. This bucket must already exist.|
9497
|s3BucketRegion|Yes|The region of the S3 bucket resides.|
98+
|copyToS3|No|If set to `true` it will copy the audit logs to the S3 bucket specified in `s3BucketName`.|
9599
|secretArn|Yes|The ARN to the secret that contains the credentials for the FSxN file systems that you want to ingest audit logs from.|
96100
|createWatchdogAlarm|No|If set to `true` it will create a CloudWatch alarm that will alert you if the Lambda function throws in error.|
97101
|snsTopicArn|No|The ARN of the SNS topic to send the alarm to. This is required if `createWatchdogAlarm` is set to `true`.|
98102
|lambdaRoleArn|No|The ARN of the role that the Lambda function will use. If not provided, the CloudFormation script will create a role for you.|
99-
|schedulreRoleArn|No|The ARN of the role that the EventBridge scheduler will run as. If not provided, the CloudFormation script will create a role for you.|
103+
|schedulerRoleArn|No|The ARN of the role that the EventBridge scheduler will run as. If not provided, the CloudFormation script will create a role for you.|
100104
|createFsxEndpoint|No|If set to `true` it will create the VPC endpoints for the FSx service|
101105
|createCloudWatchLogsEndpoint|No|If set to `true` it will create the VPC endpoints for the CloudWatch Logs service|
102106
|createSecretsManagerEndpoint|No|If set to `true` it will create the VPC endpoints for the Secrets Manager service|

Monitoring/ingest_nas_audit_logs_into_cloudwatch/cloudformation-template.yaml

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Metadata:
1414
- lambdaSecurityGroupIds
1515
- s3BucketName
1616
- s3BucketRegion
17+
- copyToS3
1718
- secretArn
1819
- createWatchdogAlarm
1920
- snsTopicArn
@@ -57,6 +58,12 @@ Parameters:
5758
Description: "The AWS region where the s3 bucket resides."
5859
Type: String
5960

61+
copyToS3:
62+
Description: "Set to 'true' if you to copy the audit log files to the S3 bucket as well as sending the individual events to the CloudWatch log stream."
63+
Type: String
64+
Default: "false"
65+
AllowedValues: ["true", "false"]
66+
6067
secretArn:
6168
Description: "The ARN of the secret that holds the FSxN credentials to use."
6269
Type: String
@@ -301,6 +308,7 @@ Resources:
301308
logGroupName: !Ref logGroupName
302309
s3BucketName: !Ref s3BucketName
303310
s3BucketRegion: !Ref s3BucketRegion
311+
copyToS3: !Ref copyToS3
304312
secretArn: !Ref secretArn
305313
statsName: "lastFileRead"
306314
volumeName: !Ref volumeName
@@ -397,7 +405,7 @@ Resources:
397405
# APIs, and then calls the ingestAuditFile function to upload the audit
398406
# log entires to the CloudWatch log group.
399407
################################################################################
400-
def processFile(ontapAdminServer, headers, volumeUUID, filePath):
408+
def readFile(ontapAdminServer, headers, volumeUUID, filePath):
401409
global http
402410
#
403411
# Create the tempoary file to hold the contents from the ONTAP/FSxN file.
@@ -407,6 +415,7 @@ Resources:
407415
# Number of bytes to read for each API call.
408416
blockSize=1024*1024
409417
418+
failed = False
410419
bytesRead = 0
411420
requestSize = 1 # Set to > 0 to start the loop.
412421
while requestSize > 0:
@@ -433,12 +442,17 @@ Resources:
433442
f.write(part.content)
434443
else:
435444
print(f'Warning: API call to {endpoint} failed. HTTP status code: {response.status}.')
445+
filed = True
436446
break
437447
438448
f.close()
439-
#
440-
# Upload the audit events to CloudWatch.
441-
ingestAuditFile(tmpFileName, filePath)
449+
if failed:
450+
os.remove(tmpFileName)
451+
return None
452+
else:
453+
return tmpFileName
454+
#
455+
# Upload the audit events to CloudWatch.
442456
443457
################################################################################
444458
# This functions converts the timestamp from the XML file to a timestamp in
@@ -592,14 +606,21 @@ Resources:
592606
'secretArn': secretArn if 'secretArn' in globals() else None, # pylint: disable=E0602
593607
's3BucketRegion': s3BucketRegion if 's3BucketRegion' in globals() else None, # pylint: disable=E0602
594608
's3BucketName': s3BucketName if 's3BucketName' in globals() else None, # pylint: disable=E0602
595-
'statsName': statsName if 'statsName' in globals() else None # pylint: disable=E0602
609+
'statsName': statsName if 'statsName' in globals() else None, # pylint: disable=E0602
610+
'copyToS3': copyToS3 if 'copyToS3' in globals() else None # pylint: disable=E0602
596611
}
612+
optionalConfig = ['copyToS3']
597613
598614
for item in config:
599615
if config[item] == None:
600616
config[item] = os.environ.get(item)
601-
if config[item] == None:
617+
if item not in optionalConfig and config[item] == None:
602618
raise Exception(f"{item} is not set.")
619+
620+
if config['copyToS3'] == None:
621+
config['copyToS3'] = False
622+
else:
623+
config['copyToS3'] = config['copyToS3'].lower() == 'true'
603624
#
604625
# To be backwards compatible, load the vserverName.
605626
config['vserverName'] = vserverName if 'vserverName' in globals() else os.environ.get('vserverName') # pylint: disable=E0602
@@ -729,14 +750,17 @@ Resources:
729750
for file in data['records']:
730751
filePath = file['name']
731752
if lastFileRead.get(fsxn) is None or lastFileRead[fsxn].get(vserverName) is None or getEpoch(filePath) > lastFileRead[fsxn][vserverName]:
732-
#
733-
# Process the file.
734-
processFile(fsxn, headersDownload, volumeUUID, filePath)
735-
if lastFileRead.get(fsxn) is None:
736-
lastFileRead[fsxn] = {vserverName: getEpoch(filePath)}
737-
else:
738-
lastFileRead[fsxn][vserverName] = getEpoch(filePath)
739-
s3Client.put_object(Key=config['statsName'], Bucket=config['s3BucketName'], Body=json.dumps(lastFileRead).encode('UTF-8'))
753+
localFileName = readFile(fsxn, headersDownload, volumeUUID, filePath)
754+
if localFileName is not None:
755+
if config['copyToS3']:
756+
s3Client.upload_file(localFileName, config['s3BucketName'], filePath)
757+
ingestAuditFile(localFileName, filePath)
758+
if lastFileRead.get(fsxn) is None:
759+
lastFileRead[fsxn] = {vserverName: getEpoch(filePath)}
760+
else:
761+
lastFileRead[fsxn][vserverName] = getEpoch(filePath)
762+
s3Client.put_object(Key=config['statsName'], Bucket=config['s3BucketName'], Body=json.dumps(lastFileRead).encode('UTF-8'))
763+
os.remove(localFileName)
740764
#
741765
# Get the next set of SVMs.
742766
if svmsData['_links'].get('next') != None:

Monitoring/ingest_nas_audit_logs_into_cloudwatch/ingest_audit_log.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def getEpoch(filename):
8989
# APIs, and then calls the ingestAuditFile function to upload the audit
9090
# log entires to the CloudWatch log group.
9191
################################################################################
92-
def processFile(ontapAdminServer, headers, volumeUUID, filePath):
92+
def readFile(ontapAdminServer, headers, volumeUUID, filePath):
9393
global http
9494
#
9595
# Create the tempoary file to hold the contents from the ONTAP/FSxN file.
@@ -99,6 +99,7 @@ def processFile(ontapAdminServer, headers, volumeUUID, filePath):
9999
# Number of bytes to read for each API call.
100100
blockSize=1024*1024
101101

102+
failed = False
102103
bytesRead = 0
103104
requestSize = 1 # Set to > 0 to start the loop.
104105
while requestSize > 0:
@@ -125,12 +126,17 @@ def processFile(ontapAdminServer, headers, volumeUUID, filePath):
125126
f.write(part.content)
126127
else:
127128
print(f'Warning: API call to {endpoint} failed. HTTP status code: {response.status}.')
129+
filed = True
128130
break
129131

130132
f.close()
131-
#
132-
# Upload the audit events to CloudWatch.
133-
ingestAuditFile(tmpFileName, filePath)
133+
if failed:
134+
os.remove(tmpFileName)
135+
return None
136+
else:
137+
return tmpFileName
138+
#
139+
# Upload the audit events to CloudWatch.
134140

135141
################################################################################
136142
# This functions converts the timestamp from the XML file to a timestamp in
@@ -284,14 +290,21 @@ def checkConfig():
284290
'secretArn': secretArn if 'secretArn' in globals() else None, # pylint: disable=E0602
285291
's3BucketRegion': s3BucketRegion if 's3BucketRegion' in globals() else None, # pylint: disable=E0602
286292
's3BucketName': s3BucketName if 's3BucketName' in globals() else None, # pylint: disable=E0602
287-
'statsName': statsName if 'statsName' in globals() else None # pylint: disable=E0602
293+
'statsName': statsName if 'statsName' in globals() else None, # pylint: disable=E0602
294+
'copyToS3': copyToS3 if 'copyToS3' in globals() else None # pylint: disable=E0602
288295
}
296+
optionalConfig = ['copyToS3']
289297

290298
for item in config:
291299
if config[item] == None:
292300
config[item] = os.environ.get(item)
293-
if config[item] == None:
301+
if item not in optionalConfig and config[item] == None:
294302
raise Exception(f"{item} is not set.")
303+
304+
if config['copyToS3'] == None:
305+
config['copyToS3'] = False
306+
else:
307+
config['copyToS3'] = config['copyToS3'].lower() == 'true'
295308
#
296309
# To be backwards compatible, load the vserverName.
297310
config['vserverName'] = vserverName if 'vserverName' in globals() else os.environ.get('vserverName') # pylint: disable=E0602
@@ -421,14 +434,17 @@ def lambda_handler(event, context): # pylint: disable=W0613
421434
for file in data['records']:
422435
filePath = file['name']
423436
if lastFileRead.get(fsxn) is None or lastFileRead[fsxn].get(vserverName) is None or getEpoch(filePath) > lastFileRead[fsxn][vserverName]:
424-
#
425-
# Process the file.
426-
processFile(fsxn, headersDownload, volumeUUID, filePath)
427-
if lastFileRead.get(fsxn) is None:
428-
lastFileRead[fsxn] = {vserverName: getEpoch(filePath)}
429-
else:
430-
lastFileRead[fsxn][vserverName] = getEpoch(filePath)
431-
s3Client.put_object(Key=config['statsName'], Bucket=config['s3BucketName'], Body=json.dumps(lastFileRead).encode('UTF-8'))
437+
localFileName = readFile(fsxn, headersDownload, volumeUUID, filePath)
438+
if localFileName is not None:
439+
if config['copyToS3']:
440+
s3Client.upload_file(localFileName, config['s3BucketName'], filePath)
441+
ingestAuditFile(localFileName, filePath)
442+
if lastFileRead.get(fsxn) is None:
443+
lastFileRead[fsxn] = {vserverName: getEpoch(filePath)}
444+
else:
445+
lastFileRead[fsxn][vserverName] = getEpoch(filePath)
446+
s3Client.put_object(Key=config['statsName'], Bucket=config['s3BucketName'], Body=json.dumps(lastFileRead).encode('UTF-8'))
447+
os.remove(localFileName)
432448
#
433449
# Get the next set of SVMs.
434450
if svmsData['_links'].get('next') != None:

0 commit comments

Comments
 (0)