tutus-chain/pkg/core/native/audit_logger.go

777 lines
23 KiB
Go

package native
import (
"encoding/binary"
"github.com/tutus-one/tutus-chain/pkg/core/dao"
"github.com/tutus-one/tutus-chain/pkg/core/storage"
"github.com/tutus-one/tutus-chain/pkg/util"
)
// ARCH-005: Comprehensive Audit Logging
// Provides structured audit logging for all sensitive operations,
// supporting compliance requirements, incident investigation, and
// security monitoring.
// AuditCategory categorizes audit events.
type AuditCategory uint8
const (
AuditCategoryAuth AuditCategory = iota
AuditCategoryAccess
AuditCategoryData
AuditCategoryGovernance
AuditCategoryFinancial
AuditCategorySecurity
AuditCategorySystem
AuditCategoryCompliance
)
// AuditSeverity indicates the importance of an audit event.
type AuditSeverity uint8
const (
AuditSeverityInfo AuditSeverity = iota
AuditSeverityNotice
AuditSeverityWarning
AuditSeverityAlert
AuditSeverityCritical
)
// AuditOutcome indicates the result of an audited operation.
type AuditOutcome uint8
const (
AuditOutcomeSuccess AuditOutcome = iota
AuditOutcomeFailure
AuditOutcomeDenied
AuditOutcomeError
AuditOutcomePartial
)
// AuditEntry represents a single audit log entry.
type AuditEntry struct {
// EntryID is unique identifier for this entry
EntryID uint64
// Timestamp is the block height when event occurred
Timestamp uint32
// Category classifies the event
Category AuditCategory
// Severity indicates importance
Severity AuditSeverity
// Outcome is the result of the operation
Outcome AuditOutcome
// ContractID identifies the originating contract
ContractID int32
// Actor is who performed the action
Actor util.Uint160
// Target is the subject of the action (if applicable)
Target util.Uint160
// Action is the operation performed
Action string
// ResourceID identifies the affected resource
ResourceID []byte
// Details contains additional context
Details string
// IPHash is hash of source IP (for off-chain correlation)
IPHash util.Uint256
// PreviousState is hash of state before change
PreviousState util.Uint256
// NewState is hash of state after change
NewState util.Uint256
}
// AuditQuery defines parameters for searching audit logs.
type AuditQuery struct {
// StartBlock is the earliest block to search
StartBlock uint32
// EndBlock is the latest block to search
EndBlock uint32
// Category filters by event category (nil = all)
Category *AuditCategory
// Severity filters by minimum severity
MinSeverity *AuditSeverity
// Actor filters by actor address
Actor *util.Uint160
// Target filters by target address
Target *util.Uint160
// ContractID filters by contract
ContractID *int32
// Limit is maximum results to return
Limit int
// Offset is the starting position
Offset int
}
// Storage prefixes for audit logging.
const (
auditPrefixEntry byte = 0xA0 // entryID -> AuditEntry
auditPrefixByBlock byte = 0xA1 // block + entryID -> exists
auditPrefixByActor byte = 0xA2 // actor + block + entryID -> exists
auditPrefixByTarget byte = 0xA3 // target + block + entryID -> exists
auditPrefixByCategory byte = 0xA4 // category + block + entryID -> exists
auditPrefixBySeverity byte = 0xA5 // severity + block + entryID -> exists
auditPrefixByContract byte = 0xA6 // contractID + block + entryID -> exists
auditPrefixCounter byte = 0xAF // -> next entryID
auditPrefixRetention byte = 0xAE // -> retention config
)
// AuditLogger provides comprehensive audit logging.
type AuditLogger struct {
contractID int32
}
// NewAuditLogger creates a new audit logger.
func NewAuditLogger(contractID int32) *AuditLogger {
return &AuditLogger{contractID: contractID}
}
// Log records an audit entry.
func (al *AuditLogger) Log(d *dao.Simple, entry *AuditEntry) uint64 {
// Get and increment entry counter
entry.EntryID = al.getNextEntryID(d)
// Store main entry
al.putEntry(d, entry)
// Create indices for efficient querying
al.indexEntry(d, entry)
return entry.EntryID
}
// LogAuth logs an authentication event.
func (al *AuditLogger) LogAuth(d *dao.Simple, actor util.Uint160, action string, outcome AuditOutcome,
blockHeight uint32, details string) uint64 {
return al.Log(d, &AuditEntry{
Timestamp: blockHeight,
Category: AuditCategoryAuth,
Severity: al.severityForOutcome(outcome, AuditCategoryAuth),
Outcome: outcome,
Actor: actor,
Action: action,
Details: details,
})
}
// LogAccess logs a data access event.
func (al *AuditLogger) LogAccess(d *dao.Simple, actor, target util.Uint160, action string,
resourceID []byte, outcome AuditOutcome, blockHeight uint32) uint64 {
return al.Log(d, &AuditEntry{
Timestamp: blockHeight,
Category: AuditCategoryAccess,
Severity: al.severityForOutcome(outcome, AuditCategoryAccess),
Outcome: outcome,
Actor: actor,
Target: target,
Action: action,
ResourceID: resourceID,
})
}
// LogDataChange logs a data modification event.
func (al *AuditLogger) LogDataChange(d *dao.Simple, actor util.Uint160, contractID int32,
action string, resourceID []byte, prevState, newState util.Uint256, blockHeight uint32) uint64 {
return al.Log(d, &AuditEntry{
Timestamp: blockHeight,
Category: AuditCategoryData,
Severity: AuditSeverityNotice,
Outcome: AuditOutcomeSuccess,
ContractID: contractID,
Actor: actor,
Action: action,
ResourceID: resourceID,
PreviousState: prevState,
NewState: newState,
})
}
// LogGovernance logs a governance action.
func (al *AuditLogger) LogGovernance(d *dao.Simple, actor util.Uint160, action string,
details string, outcome AuditOutcome, blockHeight uint32) uint64 {
severity := AuditSeverityNotice
if outcome == AuditOutcomeSuccess {
severity = AuditSeverityWarning // Governance changes warrant attention
}
return al.Log(d, &AuditEntry{
Timestamp: blockHeight,
Category: AuditCategoryGovernance,
Severity: severity,
Outcome: outcome,
Actor: actor,
Action: action,
Details: details,
})
}
// LogFinancial logs a financial transaction.
func (al *AuditLogger) LogFinancial(d *dao.Simple, actor, target util.Uint160,
action string, resourceID []byte, outcome AuditOutcome, blockHeight uint32) uint64 {
return al.Log(d, &AuditEntry{
Timestamp: blockHeight,
Category: AuditCategoryFinancial,
Severity: AuditSeverityNotice,
Outcome: outcome,
Actor: actor,
Target: target,
Action: action,
ResourceID: resourceID,
})
}
// LogSecurity logs a security-related event.
func (al *AuditLogger) LogSecurity(d *dao.Simple, actor util.Uint160, action string,
severity AuditSeverity, outcome AuditOutcome, details string, blockHeight uint32) uint64 {
return al.Log(d, &AuditEntry{
Timestamp: blockHeight,
Category: AuditCategorySecurity,
Severity: severity,
Outcome: outcome,
Actor: actor,
Action: action,
Details: details,
})
}
// LogCompliance logs a compliance-related event.
func (al *AuditLogger) LogCompliance(d *dao.Simple, actor, target util.Uint160,
action string, details string, blockHeight uint32) uint64 {
return al.Log(d, &AuditEntry{
Timestamp: blockHeight,
Category: AuditCategoryCompliance,
Severity: AuditSeverityNotice,
Outcome: AuditOutcomeSuccess,
Actor: actor,
Target: target,
Action: action,
Details: details,
})
}
// LogRightsAccess logs access to rights-protected resources.
func (al *AuditLogger) LogRightsAccess(d *dao.Simple, actor, subject util.Uint160,
rightID uint8, action string, outcome AuditOutcome, blockHeight uint32) uint64 {
return al.Log(d, &AuditEntry{
Timestamp: blockHeight,
Category: AuditCategoryCompliance,
Severity: AuditSeverityWarning,
Outcome: outcome,
Actor: actor,
Target: subject,
Action: action,
ResourceID: []byte{rightID},
})
}
// GetEntry retrieves an audit entry by ID.
func (al *AuditLogger) GetEntry(d *dao.Simple, entryID uint64) *AuditEntry {
key := al.makeEntryKey(entryID)
si := d.GetStorageItem(al.contractID, key)
if si == nil {
return nil
}
return al.deserializeEntry(si)
}
// Query searches audit logs based on query parameters.
func (al *AuditLogger) Query(d *dao.Simple, query *AuditQuery) []*AuditEntry {
var entries []*AuditEntry
var entryIDs []uint64
// Choose the most selective index
if query.Actor != nil {
entryIDs = al.queryByActor(d, *query.Actor, query.StartBlock, query.EndBlock, query.Limit+query.Offset)
} else if query.Target != nil {
entryIDs = al.queryByTarget(d, *query.Target, query.StartBlock, query.EndBlock, query.Limit+query.Offset)
} else if query.Category != nil {
entryIDs = al.queryByCategory(d, *query.Category, query.StartBlock, query.EndBlock, query.Limit+query.Offset)
} else if query.MinSeverity != nil {
entryIDs = al.queryBySeverity(d, *query.MinSeverity, query.StartBlock, query.EndBlock, query.Limit+query.Offset)
} else {
entryIDs = al.queryByBlock(d, query.StartBlock, query.EndBlock, query.Limit+query.Offset)
}
// Apply offset and limit
start := query.Offset
if start > len(entryIDs) {
return entries
}
end := start + query.Limit
if end > len(entryIDs) {
end = len(entryIDs)
}
// Fetch entries
for _, id := range entryIDs[start:end] {
if entry := al.GetEntry(d, id); entry != nil {
// Apply additional filters
if al.matchesQuery(entry, query) {
entries = append(entries, entry)
}
}
}
return entries
}
// GetEntriesForActor retrieves audit entries for a specific actor.
func (al *AuditLogger) GetEntriesForActor(d *dao.Simple, actor util.Uint160, limit int) []*AuditEntry {
return al.Query(d, &AuditQuery{
Actor: &actor,
Limit: limit,
})
}
// GetEntriesForTarget retrieves audit entries for a specific target.
func (al *AuditLogger) GetEntriesForTarget(d *dao.Simple, target util.Uint160, limit int) []*AuditEntry {
return al.Query(d, &AuditQuery{
Target: &target,
Limit: limit,
})
}
// GetSecurityAlerts retrieves high-severity security events.
func (al *AuditLogger) GetSecurityAlerts(d *dao.Simple, startBlock, endBlock uint32, limit int) []*AuditEntry {
severity := AuditSeverityAlert
category := AuditCategorySecurity
return al.Query(d, &AuditQuery{
StartBlock: startBlock,
EndBlock: endBlock,
Category: &category,
MinSeverity: &severity,
Limit: limit,
})
}
// GetComplianceLog retrieves compliance-related entries for reporting.
func (al *AuditLogger) GetComplianceLog(d *dao.Simple, startBlock, endBlock uint32, limit int) []*AuditEntry {
category := AuditCategoryCompliance
return al.Query(d, &AuditQuery{
StartBlock: startBlock,
EndBlock: endBlock,
Category: &category,
Limit: limit,
})
}
// severityForOutcome determines severity based on outcome and category.
func (al *AuditLogger) severityForOutcome(outcome AuditOutcome, category AuditCategory) AuditSeverity {
switch outcome {
case AuditOutcomeSuccess:
return AuditSeverityInfo
case AuditOutcomeFailure:
return AuditSeverityNotice
case AuditOutcomeDenied:
if category == AuditCategorySecurity || category == AuditCategoryAuth {
return AuditSeverityAlert
}
return AuditSeverityWarning
case AuditOutcomeError:
return AuditSeverityWarning
default:
return AuditSeverityInfo
}
}
// matchesQuery checks if an entry matches additional query filters.
func (al *AuditLogger) matchesQuery(entry *AuditEntry, query *AuditQuery) bool {
if query.Category != nil && entry.Category != *query.Category {
return false
}
if query.MinSeverity != nil && entry.Severity < *query.MinSeverity {
return false
}
if query.ContractID != nil && entry.ContractID != *query.ContractID {
return false
}
if query.Actor != nil && entry.Actor != *query.Actor {
return false
}
if query.Target != nil && entry.Target != *query.Target {
return false
}
return true
}
// Index query methods.
func (al *AuditLogger) queryByBlock(d *dao.Simple, startBlock, endBlock uint32, limit int) []uint64 {
var ids []uint64
prefix := []byte{auditPrefixByBlock}
d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
if len(k) < 12 {
return true
}
block := binary.BigEndian.Uint32(k[0:4])
if block < startBlock || (endBlock > 0 && block > endBlock) {
return true
}
entryID := binary.BigEndian.Uint64(k[4:12])
ids = append(ids, entryID)
return len(ids) < limit
})
return ids
}
func (al *AuditLogger) queryByActor(d *dao.Simple, actor util.Uint160, startBlock, endBlock uint32, limit int) []uint64 {
var ids []uint64
prefix := make([]byte, 21)
prefix[0] = auditPrefixByActor
copy(prefix[1:], actor.BytesBE())
d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
if len(k) < 12 {
return true
}
block := binary.BigEndian.Uint32(k[0:4])
if block < startBlock || (endBlock > 0 && block > endBlock) {
return true
}
entryID := binary.BigEndian.Uint64(k[4:12])
ids = append(ids, entryID)
return len(ids) < limit
})
return ids
}
func (al *AuditLogger) queryByTarget(d *dao.Simple, target util.Uint160, startBlock, endBlock uint32, limit int) []uint64 {
var ids []uint64
prefix := make([]byte, 21)
prefix[0] = auditPrefixByTarget
copy(prefix[1:], target.BytesBE())
d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
if len(k) < 12 {
return true
}
block := binary.BigEndian.Uint32(k[0:4])
if block < startBlock || (endBlock > 0 && block > endBlock) {
return true
}
entryID := binary.BigEndian.Uint64(k[4:12])
ids = append(ids, entryID)
return len(ids) < limit
})
return ids
}
func (al *AuditLogger) queryByCategory(d *dao.Simple, category AuditCategory, startBlock, endBlock uint32, limit int) []uint64 {
var ids []uint64
prefix := []byte{auditPrefixByCategory, byte(category)}
d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
if len(k) < 12 {
return true
}
block := binary.BigEndian.Uint32(k[0:4])
if block < startBlock || (endBlock > 0 && block > endBlock) {
return true
}
entryID := binary.BigEndian.Uint64(k[4:12])
ids = append(ids, entryID)
return len(ids) < limit
})
return ids
}
func (al *AuditLogger) queryBySeverity(d *dao.Simple, minSeverity AuditSeverity, startBlock, endBlock uint32, limit int) []uint64 {
var ids []uint64
for sev := minSeverity; sev <= AuditSeverityCritical; sev++ {
prefix := []byte{auditPrefixBySeverity, byte(sev)}
d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool {
if len(k) < 12 {
return true
}
block := binary.BigEndian.Uint32(k[0:4])
if block < startBlock || (endBlock > 0 && block > endBlock) {
return true
}
entryID := binary.BigEndian.Uint64(k[4:12])
ids = append(ids, entryID)
return len(ids) < limit
})
if len(ids) >= limit {
break
}
}
return ids
}
// Storage key helpers.
func (al *AuditLogger) makeEntryKey(entryID uint64) []byte {
key := make([]byte, 9)
key[0] = auditPrefixEntry
binary.BigEndian.PutUint64(key[1:], entryID)
return key
}
func (al *AuditLogger) getNextEntryID(d *dao.Simple) uint64 {
key := []byte{auditPrefixCounter}
si := d.GetStorageItem(al.contractID, key)
var nextID uint64 = 1
if si != nil && len(si) >= 8 {
nextID = binary.BigEndian.Uint64(si) + 1
}
data := make([]byte, 8)
binary.BigEndian.PutUint64(data, nextID)
d.PutStorageItem(al.contractID, key, data)
return nextID
}
func (al *AuditLogger) putEntry(d *dao.Simple, entry *AuditEntry) {
key := al.makeEntryKey(entry.EntryID)
data := al.serializeEntry(entry)
d.PutStorageItem(al.contractID, key, data)
}
func (al *AuditLogger) indexEntry(d *dao.Simple, entry *AuditEntry) {
// Index by block
blockKey := make([]byte, 13)
blockKey[0] = auditPrefixByBlock
binary.BigEndian.PutUint32(blockKey[1:5], entry.Timestamp)
binary.BigEndian.PutUint64(blockKey[5:13], entry.EntryID)
d.PutStorageItem(al.contractID, blockKey, []byte{1})
// Index by actor
if entry.Actor != (util.Uint160{}) {
actorKey := make([]byte, 33)
actorKey[0] = auditPrefixByActor
copy(actorKey[1:21], entry.Actor.BytesBE())
binary.BigEndian.PutUint32(actorKey[21:25], entry.Timestamp)
binary.BigEndian.PutUint64(actorKey[25:33], entry.EntryID)
d.PutStorageItem(al.contractID, actorKey, []byte{1})
}
// Index by target
if entry.Target != (util.Uint160{}) {
targetKey := make([]byte, 33)
targetKey[0] = auditPrefixByTarget
copy(targetKey[1:21], entry.Target.BytesBE())
binary.BigEndian.PutUint32(targetKey[21:25], entry.Timestamp)
binary.BigEndian.PutUint64(targetKey[25:33], entry.EntryID)
d.PutStorageItem(al.contractID, targetKey, []byte{1})
}
// Index by category
catKey := make([]byte, 14)
catKey[0] = auditPrefixByCategory
catKey[1] = byte(entry.Category)
binary.BigEndian.PutUint32(catKey[2:6], entry.Timestamp)
binary.BigEndian.PutUint64(catKey[6:14], entry.EntryID)
d.PutStorageItem(al.contractID, catKey, []byte{1})
// Index by severity
sevKey := make([]byte, 14)
sevKey[0] = auditPrefixBySeverity
sevKey[1] = byte(entry.Severity)
binary.BigEndian.PutUint32(sevKey[2:6], entry.Timestamp)
binary.BigEndian.PutUint64(sevKey[6:14], entry.EntryID)
d.PutStorageItem(al.contractID, sevKey, []byte{1})
// Index by contract
if entry.ContractID != 0 {
contractKey := make([]byte, 17)
contractKey[0] = auditPrefixByContract
binary.BigEndian.PutUint32(contractKey[1:5], uint32(entry.ContractID))
binary.BigEndian.PutUint32(contractKey[5:9], entry.Timestamp)
binary.BigEndian.PutUint64(contractKey[9:17], entry.EntryID)
d.PutStorageItem(al.contractID, contractKey, []byte{1})
}
}
// Serialization helpers.
func (al *AuditLogger) serializeEntry(e *AuditEntry) []byte {
actionBytes := []byte(e.Action)
detailsBytes := []byte(e.Details)
size := 8 + 4 + 1 + 1 + 1 + 4 + 20 + 20 +
4 + len(actionBytes) +
4 + len(e.ResourceID) +
4 + len(detailsBytes) +
32 + 32 + 32
data := make([]byte, size)
offset := 0
binary.BigEndian.PutUint64(data[offset:], e.EntryID)
offset += 8
binary.BigEndian.PutUint32(data[offset:], e.Timestamp)
offset += 4
data[offset] = byte(e.Category)
offset++
data[offset] = byte(e.Severity)
offset++
data[offset] = byte(e.Outcome)
offset++
binary.BigEndian.PutUint32(data[offset:], uint32(e.ContractID))
offset += 4
copy(data[offset:], e.Actor.BytesBE())
offset += 20
copy(data[offset:], e.Target.BytesBE())
offset += 20
binary.BigEndian.PutUint32(data[offset:], uint32(len(actionBytes)))
offset += 4
copy(data[offset:], actionBytes)
offset += len(actionBytes)
binary.BigEndian.PutUint32(data[offset:], uint32(len(e.ResourceID)))
offset += 4
copy(data[offset:], e.ResourceID)
offset += len(e.ResourceID)
binary.BigEndian.PutUint32(data[offset:], uint32(len(detailsBytes)))
offset += 4
copy(data[offset:], detailsBytes)
offset += len(detailsBytes)
copy(data[offset:], e.IPHash[:])
offset += 32
copy(data[offset:], e.PreviousState[:])
offset += 32
copy(data[offset:], e.NewState[:])
return data
}
func (al *AuditLogger) deserializeEntry(data []byte) *AuditEntry {
if len(data) < 60 {
return nil
}
e := &AuditEntry{}
offset := 0
e.EntryID = binary.BigEndian.Uint64(data[offset:])
offset += 8
e.Timestamp = binary.BigEndian.Uint32(data[offset:])
offset += 4
e.Category = AuditCategory(data[offset])
offset++
e.Severity = AuditSeverity(data[offset])
offset++
e.Outcome = AuditOutcome(data[offset])
offset++
e.ContractID = int32(binary.BigEndian.Uint32(data[offset:]))
offset += 4
e.Actor, _ = util.Uint160DecodeBytesBE(data[offset : offset+20])
offset += 20
e.Target, _ = util.Uint160DecodeBytesBE(data[offset : offset+20])
offset += 20
if offset+4 > len(data) {
return nil
}
actionLen := binary.BigEndian.Uint32(data[offset:])
offset += 4
if offset+int(actionLen) > len(data) {
return nil
}
e.Action = string(data[offset : offset+int(actionLen)])
offset += int(actionLen)
if offset+4 > len(data) {
return nil
}
resourceLen := binary.BigEndian.Uint32(data[offset:])
offset += 4
if offset+int(resourceLen) > len(data) {
return nil
}
e.ResourceID = make([]byte, resourceLen)
copy(e.ResourceID, data[offset:offset+int(resourceLen)])
offset += int(resourceLen)
if offset+4 > len(data) {
return nil
}
detailsLen := binary.BigEndian.Uint32(data[offset:])
offset += 4
if offset+int(detailsLen) > len(data) {
return nil
}
e.Details = string(data[offset : offset+int(detailsLen)])
offset += int(detailsLen)
if offset+96 > len(data) {
return e // Return partial if hashes missing
}
copy(e.IPHash[:], data[offset:offset+32])
offset += 32
copy(e.PreviousState[:], data[offset:offset+32])
offset += 32
copy(e.NewState[:], data[offset:offset+32])
return e
}
// StandardAuditActions defines common audit action names.
var StandardAuditActions = struct {
// Auth actions
AuthLogin string
AuthLogout string
AuthFailedLogin string
AuthRoleAssigned string
AuthRoleRevoked string
// Access actions
AccessRead string
AccessWrite string
AccessDelete string
AccessDenied string
// Financial actions
FinTransfer string
FinMint string
FinBurn string
FinInvest string
FinWithdraw string
// Governance actions
GovProposalCreate string
GovVote string
GovProposalPass string
GovProposalReject string
// Security actions
SecCircuitTrip string
SecRollback string
SecInvariantFail string
SecRightsRestrict string
}{
AuthLogin: "auth.login",
AuthLogout: "auth.logout",
AuthFailedLogin: "auth.failed_login",
AuthRoleAssigned: "auth.role_assigned",
AuthRoleRevoked: "auth.role_revoked",
AccessRead: "access.read",
AccessWrite: "access.write",
AccessDelete: "access.delete",
AccessDenied: "access.denied",
FinTransfer: "financial.transfer",
FinMint: "financial.mint",
FinBurn: "financial.burn",
FinInvest: "financial.invest",
FinWithdraw: "financial.withdraw",
GovProposalCreate: "governance.proposal_create",
GovVote: "governance.vote",
GovProposalPass: "governance.proposal_pass",
GovProposalReject: "governance.proposal_reject",
SecCircuitTrip: "security.circuit_trip",
SecRollback: "security.rollback",
SecInvariantFail: "security.invariant_fail",
SecRightsRestrict: "security.rights_restrict",
}