Skip to content

Commit 9f130b9

Browse files
committed
Add DATABASE AUDIT SPECIFICATION action and group support
- Add AuditActionSpecification and DatabaseAuditAction types - Add DropDatabaseAuditSpecificationStatement - Update parseAuditSpecificationPart to handle action specs - Add batch group mappings (BatchCompletedGroup, BatchStartedGroup) Enables: DatabaseAuditSpecificationStatementTests (all 4 variants)
1 parent aa2f616 commit 9f130b9

8 files changed

Lines changed: 237 additions & 34 deletions

File tree

ast/server_audit_statement.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,30 @@ type AuditActionGroupReference struct {
205205

206206
func (r *AuditActionGroupReference) node() {}
207207
func (r *AuditActionGroupReference) auditSpecificationDetail() {}
208+
209+
// AuditActionSpecification represents an action specification in audit parts
210+
// Example: (select, INSERT, update ON t1 BY dbo)
211+
type AuditActionSpecification struct {
212+
Actions []*DatabaseAuditAction
213+
Principals []*SecurityPrincipal
214+
TargetObject *SecurityTargetObject
215+
}
216+
217+
func (a *AuditActionSpecification) node() {}
218+
func (a *AuditActionSpecification) auditSpecificationDetail() {}
219+
220+
// DatabaseAuditAction represents a database audit action
221+
type DatabaseAuditAction struct {
222+
ActionKind string // Select, Insert, Update, Delete, Execute, Receive, References
223+
}
224+
225+
func (a *DatabaseAuditAction) node() {}
226+
227+
// DropDatabaseAuditSpecificationStatement represents DROP DATABASE AUDIT SPECIFICATION
228+
type DropDatabaseAuditSpecificationStatement struct {
229+
Name *Identifier
230+
IsIfExists bool
231+
}
232+
233+
func (s *DropDatabaseAuditSpecificationStatement) statement() {}
234+
func (s *DropDatabaseAuditSpecificationStatement) node() {}

parser/marshal.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ func statementToJSON(stmt ast.Statement) jsonNode {
200200
return dropServerRoleStatementToJSON(s)
201201
case *ast.DropServerAuditStatement:
202202
return dropServerAuditStatementToJSON(s)
203+
case *ast.DropDatabaseAuditSpecificationStatement:
204+
return dropDatabaseAuditSpecificationStatementToJSON(s)
203205
case *ast.DropAvailabilityGroupStatement:
204206
return dropAvailabilityGroupStatementToJSON(s)
205207
case *ast.DropFederationStatement:
@@ -8854,6 +8856,38 @@ func auditSpecificationDetailToJSON(d ast.AuditSpecificationDetail) jsonNode {
88548856
"$type": "AuditActionGroupReference",
88558857
"Group": detail.Group,
88568858
}
8859+
case *ast.AuditActionSpecification:
8860+
node := jsonNode{
8861+
"$type": "AuditActionSpecification",
8862+
}
8863+
if len(detail.Actions) > 0 {
8864+
actions := make([]jsonNode, len(detail.Actions))
8865+
for i, a := range detail.Actions {
8866+
actions[i] = jsonNode{
8867+
"$type": "DatabaseAuditAction",
8868+
"ActionKind": a.ActionKind,
8869+
}
8870+
}
8871+
node["Actions"] = actions
8872+
}
8873+
if len(detail.Principals) > 0 {
8874+
principals := make([]jsonNode, len(detail.Principals))
8875+
for i, p := range detail.Principals {
8876+
principalNode := jsonNode{
8877+
"$type": "SecurityPrincipal",
8878+
"PrincipalType": p.PrincipalType,
8879+
}
8880+
if p.Identifier != nil {
8881+
principalNode["Identifier"] = identifierToJSON(p.Identifier)
8882+
}
8883+
principals[i] = principalNode
8884+
}
8885+
node["Principals"] = principals
8886+
}
8887+
if detail.TargetObject != nil {
8888+
node["TargetObject"] = securityTargetObjectToJSON(detail.TargetObject)
8889+
}
8890+
return node
88578891
default:
88588892
return jsonNode{}
88598893
}
@@ -8870,6 +8904,17 @@ func dropServerAuditStatementToJSON(s *ast.DropServerAuditStatement) jsonNode {
88708904
return node
88718905
}
88728906

8907+
func dropDatabaseAuditSpecificationStatementToJSON(s *ast.DropDatabaseAuditSpecificationStatement) jsonNode {
8908+
node := jsonNode{
8909+
"$type": "DropDatabaseAuditSpecificationStatement",
8910+
"IsIfExists": s.IsIfExists,
8911+
}
8912+
if s.Name != nil {
8913+
node["Name"] = identifierToJSON(s.Name)
8914+
}
8915+
return node
8916+
}
8917+
88738918
func auditTargetToJSON(t *ast.AuditTarget) jsonNode {
88748919
node := jsonNode{
88758920
"$type": "AuditTarget",

parser/parse_ddl.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,25 @@ func (p *Parser) parseDropDatabaseStatement() (ast.Statement, error) {
906906
// Consume DATABASE
907907
p.nextToken()
908908

909+
// Check for DATABASE AUDIT SPECIFICATION
910+
if strings.ToUpper(p.curTok.Literal) == "AUDIT" {
911+
p.nextToken() // consume AUDIT
912+
if strings.ToUpper(p.curTok.Literal) == "SPECIFICATION" {
913+
p.nextToken() // consume SPECIFICATION
914+
}
915+
stmt := &ast.DropDatabaseAuditSpecificationStatement{}
916+
// Check for IF EXISTS
917+
if strings.ToUpper(p.curTok.Literal) == "IF" {
918+
p.nextToken()
919+
if strings.ToUpper(p.curTok.Literal) == "EXISTS" {
920+
p.nextToken()
921+
stmt.IsIfExists = true
922+
}
923+
}
924+
stmt.Name = p.parseIdentifier()
925+
return stmt, nil
926+
}
927+
909928
// Check for DATABASE ENCRYPTION KEY
910929
if strings.ToUpper(p.curTok.Literal) == "ENCRYPTION" {
911930
p.nextToken() // consume ENCRYPTION

parser/parse_statements.go

Lines changed: 142 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3649,21 +3649,9 @@ func (p *Parser) parseCreateDatabaseAuditSpecificationStatement() (*ast.CreateDa
36493649
for {
36503650
upperLit := strings.ToUpper(p.curTok.Literal)
36513651
if upperLit == "ADD" || upperLit == "DROP" {
3652-
part := &ast.AuditSpecificationPart{
3653-
IsDrop: upperLit == "DROP",
3654-
}
3655-
p.nextToken() // consume ADD/DROP
3656-
if p.curTok.Type == TokenLParen {
3657-
p.nextToken() // consume (
3658-
// Parse audit action group reference
3659-
groupName := p.curTok.Literal
3660-
part.Details = &ast.AuditActionGroupReference{
3661-
Group: convertAuditGroupName(groupName),
3662-
}
3663-
p.nextToken() // consume group name
3664-
if p.curTok.Type == TokenRParen {
3665-
p.nextToken() // consume )
3666-
}
3652+
part, err := p.parseAuditSpecificationPart(upperLit == "DROP")
3653+
if err != nil {
3654+
return nil, err
36673655
}
36683656
stmt.Parts = append(stmt.Parts, part)
36693657
if p.curTok.Type == TokenComma {
@@ -3724,21 +3712,9 @@ func (p *Parser) parseAlterDatabaseAuditSpecificationStatement() (*ast.AlterData
37243712
for {
37253713
upperLit := strings.ToUpper(p.curTok.Literal)
37263714
if upperLit == "ADD" || upperLit == "DROP" {
3727-
part := &ast.AuditSpecificationPart{
3728-
IsDrop: upperLit == "DROP",
3729-
}
3730-
p.nextToken() // consume ADD/DROP
3731-
if p.curTok.Type == TokenLParen {
3732-
p.nextToken() // consume (
3733-
// Parse audit action group reference
3734-
groupName := p.curTok.Literal
3735-
part.Details = &ast.AuditActionGroupReference{
3736-
Group: convertAuditGroupName(groupName),
3737-
}
3738-
p.nextToken() // consume group name
3739-
if p.curTok.Type == TokenRParen {
3740-
p.nextToken() // consume )
3741-
}
3715+
part, err := p.parseAuditSpecificationPart(upperLit == "DROP")
3716+
if err != nil {
3717+
return nil, err
37423718
}
37433719
stmt.Parts = append(stmt.Parts, part)
37443720
if p.curTok.Type == TokenComma {
@@ -3784,13 +3760,149 @@ func convertAuditGroupName(name string) string {
37843760
"DATABASE_LOGOUT_GROUP": "DatabaseLogoutGroup",
37853761
"USER_CHANGE_PASSWORD_GROUP": "UserChangePasswordGroup",
37863762
"USER_DEFINED_AUDIT_GROUP": "UserDefinedAuditGroup",
3763+
"DATABASE_PERMISSION_CHANGE_GROUP": "DatabasePermissionChange",
3764+
"SCHEMA_OBJECT_PERMISSION_CHANGE_GROUP": "SchemaObjectPermissionChange",
3765+
"DATABASE_ROLE_MEMBER_CHANGE_GROUP": "DatabaseRoleMemberChange",
3766+
"APPLICATION_ROLE_CHANGE_PASSWORD_GROUP": "ApplicationRoleChangePassword",
3767+
"SCHEMA_OBJECT_ACCESS_GROUP": "SchemaObjectAccess",
3768+
"BACKUP_RESTORE_GROUP": "BackupRestore",
3769+
"DBCC_GROUP": "Dbcc",
3770+
"AUDIT_CHANGE_GROUP": "AuditChange",
3771+
"DATABASE_CHANGE_GROUP": "DatabaseChange",
3772+
"DATABASE_OBJECT_CHANGE_GROUP": "DatabaseObjectChange",
3773+
"DATABASE_PRINCIPAL_CHANGE_GROUP": "DatabasePrincipalChange",
3774+
"SCHEMA_OBJECT_CHANGE_GROUP": "SchemaObjectChange",
3775+
"DATABASE_PRINCIPAL_IMPERSONATION_GROUP": "DatabasePrincipalImpersonation",
3776+
"DATABASE_OBJECT_OWNERSHIP_CHANGE_GROUP": "DatabaseObjectOwnershipChange",
3777+
"DATABASE_OWNERSHIP_CHANGE_GROUP": "DatabaseOwnershipChange",
3778+
"SCHEMA_OBJECT_OWNERSHIP_CHANGE_GROUP": "SchemaObjectOwnershipChange",
3779+
"DATABASE_OBJECT_PERMISSION_CHANGE_GROUP": "DatabaseObjectPermissionChange",
3780+
"DATABASE_OPERATION_GROUP": "DatabaseOperation",
3781+
"DATABASE_OBJECT_ACCESS_GROUP": "DatabaseObjectAccess",
3782+
"BATCH_COMPLETED_GROUP": "BatchCompletedGroup",
3783+
"BATCH_STARTED_GROUP": "BatchStartedGroup",
37873784
}
37883785
if mapped, ok := groupMap[strings.ToUpper(name)]; ok {
37893786
return mapped
37903787
}
37913788
return capitalizeFirst(strings.ToLower(strings.ReplaceAll(name, "_", " ")))
37923789
}
37933790

3791+
// isAuditAction checks if the given word is a database audit action
3792+
func isAuditAction(word string) bool {
3793+
actions := map[string]bool{
3794+
"SELECT": true, "INSERT": true, "UPDATE": true, "DELETE": true,
3795+
"EXECUTE": true, "RECEIVE": true, "REFERENCES": true,
3796+
}
3797+
return actions[word]
3798+
}
3799+
3800+
// convertAuditActionKind converts audit action to expected format
3801+
func convertAuditActionKind(action string) string {
3802+
actionMap := map[string]string{
3803+
"SELECT": "Select",
3804+
"INSERT": "Insert",
3805+
"UPDATE": "Update",
3806+
"DELETE": "Delete",
3807+
"EXECUTE": "Execute",
3808+
"RECEIVE": "Receive",
3809+
"REFERENCES": "References",
3810+
}
3811+
if mapped, ok := actionMap[action]; ok {
3812+
return mapped
3813+
}
3814+
return capitalizeFirst(strings.ToLower(action))
3815+
}
3816+
3817+
// parseAuditSpecificationPart parses an ADD or DROP part of an audit specification
3818+
func (p *Parser) parseAuditSpecificationPart(isDrop bool) (*ast.AuditSpecificationPart, error) {
3819+
part := &ast.AuditSpecificationPart{
3820+
IsDrop: isDrop,
3821+
}
3822+
p.nextToken() // consume ADD/DROP
3823+
3824+
if p.curTok.Type == TokenLParen {
3825+
p.nextToken() // consume (
3826+
3827+
// Check if it's an action specification (SELECT, INSERT, etc.) or an audit group
3828+
firstWord := strings.ToUpper(p.curTok.Literal)
3829+
if isAuditAction(firstWord) {
3830+
// Parse action specification
3831+
spec := &ast.AuditActionSpecification{}
3832+
3833+
// Parse actions
3834+
for {
3835+
actionKind := convertAuditActionKind(strings.ToUpper(p.curTok.Literal))
3836+
spec.Actions = append(spec.Actions, &ast.DatabaseAuditAction{ActionKind: actionKind})
3837+
p.nextToken()
3838+
if p.curTok.Type == TokenComma {
3839+
p.nextToken()
3840+
// Check if next is ON (end of actions) or another action
3841+
if strings.ToUpper(p.curTok.Literal) == "ON" {
3842+
break
3843+
}
3844+
} else {
3845+
break
3846+
}
3847+
}
3848+
3849+
// Parse ON object
3850+
if strings.ToUpper(p.curTok.Literal) == "ON" {
3851+
p.nextToken() // consume ON
3852+
objIdent := p.parseIdentifier()
3853+
spec.TargetObject = &ast.SecurityTargetObject{
3854+
ObjectKind: "NotSpecified",
3855+
ObjectName: &ast.SecurityTargetObjectName{
3856+
MultiPartIdentifier: &ast.MultiPartIdentifier{
3857+
Identifiers: []*ast.Identifier{objIdent},
3858+
Count: 1,
3859+
},
3860+
},
3861+
}
3862+
}
3863+
3864+
// Parse BY principals
3865+
if strings.ToUpper(p.curTok.Literal) == "BY" {
3866+
p.nextToken() // consume BY
3867+
for {
3868+
principal := &ast.SecurityPrincipal{}
3869+
upper := strings.ToUpper(p.curTok.Literal)
3870+
if upper == "PUBLIC" {
3871+
principal.PrincipalType = "Public"
3872+
p.nextToken()
3873+
} else if upper == "NULL" {
3874+
principal.PrincipalType = "Null"
3875+
p.nextToken()
3876+
} else {
3877+
principal.PrincipalType = "Identifier"
3878+
principal.Identifier = p.parseIdentifier()
3879+
}
3880+
spec.Principals = append(spec.Principals, principal)
3881+
if p.curTok.Type == TokenComma {
3882+
p.nextToken()
3883+
} else {
3884+
break
3885+
}
3886+
}
3887+
}
3888+
part.Details = spec
3889+
} else {
3890+
// Parse audit action group reference
3891+
groupName := p.curTok.Literal
3892+
part.Details = &ast.AuditActionGroupReference{
3893+
Group: convertAuditGroupName(groupName),
3894+
}
3895+
p.nextToken() // consume group name
3896+
}
3897+
3898+
if p.curTok.Type == TokenRParen {
3899+
p.nextToken() // consume )
3900+
}
3901+
}
3902+
3903+
return part, nil
3904+
}
3905+
37943906
func (p *Parser) parseAuditTarget() (*ast.AuditTarget, error) {
37953907
target := &ast.AuditTarget{}
37963908

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}

0 commit comments

Comments
 (0)