Skip to content

Commit 70a0765

Browse files
committed
♻️ refactor: update codebase #2
1 parent 61041e0 commit 70a0765

6 files changed

Lines changed: 1536 additions & 7 deletions

File tree

.gitignore

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,8 @@ logs/
3939
upload/
4040
dir/
4141
config/conf-local.yaml
42+
bin
43+
output/
4244

4345
# Main
44-
main/
45-
main.go
46-
47-
# Examples (must be explicitly included to override the main.go rule above)
48-
!_example/
49-
!_example/**
50-
!_example/**/*.go
46+
main/main.go

examples/audit/main.go

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
// Package main demonstrates an enterprise audit-trail system using timefy.
2+
//
3+
// Real-world scenario: a financial services platform must:
4+
//
5+
// 1. Record every data-access event with nanosecond-precision UTC timestamps.
6+
// 2. Enforce a GDPR Article 33 compliance rule: any personal-data breach must
7+
// be reported to the supervisory authority within 72 hours of detection.
8+
// 3. Detect SLA breaches (e.g., a support ticket must be resolved within
9+
// 4 business hours of creation).
10+
// 4. Generate a daily digest of events grouped by hour for the CISO dashboard.
11+
// 5. Answer "how long ago was this event?" in human-readable form for the UI.
12+
//
13+
// Run with:
14+
//
15+
// cd _example/audit && go run main.go
16+
package main
17+
18+
import (
19+
"fmt"
20+
"sort"
21+
"time"
22+
23+
"github.com/sivaosorg/timefy"
24+
)
25+
26+
// Severity classifies an audit event.
27+
type Severity string
28+
29+
const (
30+
SeverityInfo Severity = "INFO"
31+
SeverityWarning Severity = "WARNING"
32+
SeverityCritical Severity = "CRITICAL"
33+
SeverityBreach Severity = "BREACH"
34+
)
35+
36+
// AuditEvent is a single immutable audit record.
37+
type AuditEvent struct {
38+
ID string
39+
OccurredAt time.Time // stored in UTC, nanosecond precision
40+
ActorID string
41+
Action string
42+
Resource string
43+
Severity Severity
44+
Resolved bool
45+
ResolvedAt time.Time // zero value if unresolved
46+
}
47+
48+
// HourBucket groups audit events that occurred within the same UTC hour.
49+
type HourBucket struct {
50+
Hour time.Time // beginning of the hour (UTC)
51+
Events []AuditEvent
52+
}
53+
54+
// ─────────────────────────────────────────────────────────────────────────────
55+
// gdprBreachDeadline returns the timestamp by which a detected breach must be
56+
// reported. GDPR Article 33 mandates notification within 72 hours.
57+
//
58+
// Why timefy.AddHour(detectedAt, 72)?
59+
// - Using time.Add(72 * time.Hour) is equivalent here, but AddHour reads as
60+
// intent-revealing code in an audit context where "hours" is the business
61+
// unit mandated by law.
62+
// ─────────────────────────────────────────────────────────────────────────────
63+
func gdprBreachDeadline(detectedAt time.Time) time.Time {
64+
return timefy.AddHour(detectedAt, 72)
65+
}
66+
67+
// isGDPRCompliant reports whether a breach detected at `detectedAt` has been
68+
// reported (i.e., is resolved) within the 72-hour window from `reportedAt`.
69+
func isGDPRCompliant(detectedAt, reportedAt time.Time) bool {
70+
deadline := gdprBreachDeadline(detectedAt)
71+
// IsWithinTolerance would be too loose (±1 minute). We use a direct
72+
// comparison: reported time must be before or exactly at the deadline.
73+
return !reportedAt.After(deadline)
74+
}
75+
76+
// ─────────────────────────────────────────────────────────────────────────────
77+
// slaDeadline returns the SLA resolution deadline for a ticket created at
78+
// `createdAt`. The SLA clock runs for `businessHours` business hours (M-F,
79+
// 09:00-18:00 UTC for simplicity; adapt WithLocation for regional SLAs).
80+
//
81+
// Algorithm:
82+
// 1. Start at createdAt.
83+
// 2. Add one hour at a time, counting only hours that fall within business
84+
// hours on weekdays, until the required number of hours is consumed.
85+
//
86+
// Why not simply use AddHour(createdAt, businessHours)?
87+
// - SLAs in enterprise support contracts only count working hours.
88+
// A ticket created at 17:00 Friday with a 4-hour SLA should expire at
89+
// 13:00 Monday, not 21:00 Friday (which would already be closed).
90+
// ─────────────────────────────────────────────────────────────────────────────
91+
func slaDeadline(createdAt time.Time, businessHours int) time.Time {
92+
cursor := createdAt
93+
remaining := businessHours
94+
95+
for remaining > 0 {
96+
cursor = timefy.AddHour(cursor, 1)
97+
tx := timefy.New(cursor)
98+
h := cursor.Hour()
99+
if tx.IsWeekday() && h >= 9 && h < 18 {
100+
remaining--
101+
}
102+
}
103+
return cursor
104+
}
105+
106+
// isSLABreached reports whether an event is unresolved and its SLA deadline
107+
// has passed.
108+
func isSLABreached(event AuditEvent, slaHours int) bool {
109+
if event.Resolved {
110+
return false
111+
}
112+
deadline := slaDeadline(event.OccurredAt, slaHours)
113+
return timefy.New(deadline).IsPast()
114+
}
115+
116+
// ─────────────────────────────────────────────────────────────────────────────
117+
// bucketByHour groups a slice of AuditEvents into HourBuckets sorted by the
118+
// UTC hour in which they occurred.
119+
//
120+
// BeginningOfMinute (Truncate to minute) then BeginningOfHour gives us the
121+
// canonical hour key. Using timefy.New(e.OccurredAt).BeginningOfHour() is
122+
// simpler and more readable than the equivalent time.Truncate(time.Hour).
123+
// ─────────────────────────────────────────────────────────────────────────────
124+
func bucketByHour(events []AuditEvent) []HourBucket {
125+
bucketMap := make(map[time.Time]*HourBucket)
126+
127+
for _, e := range events {
128+
hourKey := timefy.New(e.OccurredAt).BeginningOfHour()
129+
if _, exists := bucketMap[hourKey]; !exists {
130+
bucketMap[hourKey] = &HourBucket{Hour: hourKey}
131+
}
132+
bucketMap[hourKey].Events = append(bucketMap[hourKey].Events, e)
133+
}
134+
135+
// Sort buckets chronologically.
136+
buckets := make([]HourBucket, 0, len(bucketMap))
137+
for _, b := range bucketMap {
138+
buckets = append(buckets, *b)
139+
}
140+
sort.Slice(buckets, func(i, j int) bool {
141+
return buckets[i].Hour.Before(buckets[j].Hour)
142+
})
143+
return buckets
144+
}
145+
146+
// ─────────────────────────────────────────────────────────────────────────────
147+
// eventsInWindow returns all events whose OccurredAt falls within [start, end].
148+
//
149+
// IsBetween(start, end) performs an inclusive range check on the Timex, so
150+
// events exactly at midnight or exactly at end-of-day are correctly included.
151+
// ─────────────────────────────────────────────────────────────────────────────
152+
func eventsInWindow(events []AuditEvent, start, end time.Time) []AuditEvent {
153+
var result []AuditEvent
154+
for _, e := range events {
155+
if timefy.New(e.OccurredAt).IsBetween(start, end) {
156+
result = append(result, e)
157+
}
158+
}
159+
return result
160+
}
161+
162+
// ─────────────────────────────────────────────────────────────────────────────
163+
// formatComponents uses FormatTimex to extract individual time components for
164+
// a structured audit log line.
165+
//
166+
// FormatTimex returns [nanosecond, second, minute, hour, day, month, year]
167+
// which maps perfectly to a structured log field set.
168+
// ─────────────────────────────────────────────────────────────────────────────
169+
func structuredTimestamp(t time.Time) string {
170+
c := timefy.FormatTimex(t)
171+
// c = [ns, sec, min, hour, day, month, year]
172+
return fmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02d.%09d UTC",
173+
c[6], c[5], c[4], c[3], c[2], c[1], c[0])
174+
}
175+
176+
func main() {
177+
// ── Synthetic audit log (timestamps carefully chosen to exercise each rule)
178+
179+
// Reference "now" for this demo run.
180+
now := time.Date(2025, time.March, 24, 14, 30, 0, 0, time.UTC)
181+
182+
events := []AuditEvent{
183+
// Normal access events today
184+
{
185+
ID: "evt-001", OccurredAt: time.Date(2025, 3, 24, 8, 15, 0, 0, time.UTC),
186+
ActorID: "alice", Action: "READ", Resource: "/api/users/42",
187+
Severity: SeverityInfo,
188+
},
189+
{
190+
ID: "evt-002", OccurredAt: time.Date(2025, 3, 24, 9, 5, 30, 123456789, time.UTC),
191+
ActorID: "bob", Action: "WRITE", Resource: "/api/payments/99",
192+
Severity: SeverityInfo,
193+
},
194+
// Warning: unusual bulk export
195+
{
196+
ID: "evt-003", OccurredAt: time.Date(2025, 3, 24, 10, 45, 0, 0, time.UTC),
197+
ActorID: "carol", Action: "EXPORT", Resource: "/api/users (bulk)",
198+
Severity: SeverityWarning,
199+
},
200+
// Critical: privilege escalation attempted
201+
{
202+
ID: "evt-004", OccurredAt: time.Date(2025, 3, 24, 11, 0, 0, 0, time.UTC),
203+
ActorID: "dave", Action: "ESCALATE", Resource: "/admin/roles",
204+
Severity: SeverityCritical, Resolved: false,
205+
},
206+
// BREACH: detected at 08:00, reported at 10:00 on the same day
207+
{
208+
ID: "evt-005", OccurredAt: time.Date(2025, 3, 22, 8, 0, 0, 0, time.UTC),
209+
ActorID: "system", Action: "DATA_LEAK", Resource: "/db/pii_records",
210+
Severity: SeverityBreach, Resolved: true,
211+
ResolvedAt: time.Date(2025, 3, 22, 10, 0, 0, 0, time.UTC),
212+
},
213+
// BREACH: detected Saturday, reported 80 hours later — NON-COMPLIANT
214+
{
215+
ID: "evt-006", OccurredAt: time.Date(2025, 3, 21, 6, 0, 0, 0, time.UTC),
216+
ActorID: "system", Action: "UNAUTHORIZED_ACCESS", Resource: "/api/credit_cards",
217+
Severity: SeverityBreach, Resolved: true,
218+
ResolvedAt: time.Date(2025, 3, 24, 14, 0, 0, 0, time.UTC), // 80 hours later
219+
},
220+
// Unresolved critical ticket — created Friday 17:30 UTC, SLA = 4 biz hours
221+
{
222+
ID: "evt-007", OccurredAt: time.Date(2025, 3, 21, 17, 30, 0, 0, time.UTC),
223+
ActorID: "frank", Action: "ACCESS_DENIED", Resource: "/api/reports",
224+
Severity: SeverityCritical, Resolved: false,
225+
},
226+
}
227+
228+
fmt.Println("═══════════════════════════════════════════════════════════")
229+
fmt.Println(" TIMEFY — ENTERPRISE AUDIT TRAIL DEMO ")
230+
fmt.Println("═══════════════════════════════════════════════════════════")
231+
232+
// ── 1. Full audit log with human-readable age ─────────────────────────────
233+
fmt.Println("\n Full audit log:")
234+
fmt.Println(" ─────────────────────────────────────────────────────────────────────────")
235+
// We override "now" for TimeAgo by using DurationInDays / hours manually
236+
// when we need a frozen reference. Standard TimeAgo() uses time.Now()
237+
// internally, which is correct for a live system.
238+
for _, e := range events {
239+
age := timefy.New(e.OccurredAt).TimeAgo()
240+
ts := structuredTimestamp(e.OccurredAt)
241+
fmt.Printf(" [%s] %-12s %-15s %-10s %s (%s)\n",
242+
e.Severity, e.Action, e.Resource, e.ActorID, ts, age)
243+
}
244+
245+
// ── 2. GDPR Article 33 compliance check ───────────────────────────────────
246+
fmt.Println("\n GDPR Article 33 compliance (72-hour notification rule):")
247+
fmt.Println(" ─────────────────────────────────────────────────────────")
248+
for _, e := range events {
249+
if e.Severity != SeverityBreach {
250+
continue
251+
}
252+
deadline := gdprBreachDeadline(e.OccurredAt)
253+
if e.Resolved {
254+
compliant := isGDPRCompliant(e.OccurredAt, e.ResolvedAt)
255+
hoursUsed := int(e.ResolvedAt.Sub(e.OccurredAt).Hours())
256+
icon := "✓ COMPLIANT"
257+
if !compliant {
258+
icon = "✗ NON-COMPLIANT"
259+
}
260+
fmt.Printf(" [%s] event %s detected %s deadline %s reported in %dh → %s\n",
261+
e.Severity, e.ID,
262+
e.OccurredAt.Format("01-02 15:04"),
263+
deadline.Format("01-02 15:04"),
264+
hoursUsed, icon)
265+
}
266+
}
267+
268+
// ── 3. SLA breach detection ────────────────────────────────────────────────
269+
fmt.Println("\n SLA breach detection (4 business hours):")
270+
fmt.Println(" ─────────────────────────────────────────────────────────")
271+
for _, e := range events {
272+
if e.Severity != SeverityCritical {
273+
continue
274+
}
275+
deadline := slaDeadline(e.OccurredAt, 4)
276+
breached := isSLABreached(e, 4)
277+
hoursElapsed := int(now.Sub(e.OccurredAt).Hours())
278+
icon := "✓ within SLA"
279+
if breached {
280+
icon = "✗ SLA BREACHED"
281+
}
282+
fmt.Printf(" event %-8s created %s SLA deadline %s elapsed %dh → %s\n",
283+
e.ID,
284+
e.OccurredAt.Format("Mon 01-02 15:04"),
285+
deadline.Format("Mon 01-02 15:04"),
286+
hoursElapsed, icon)
287+
}
288+
289+
// ── 4. Events in today's window ───────────────────────────────────────────
290+
todayStart := timefy.New(now).BeginningOfDay()
291+
todayEnd := timefy.New(now).EndOfDay()
292+
todayEvents := eventsInWindow(events, todayStart, todayEnd)
293+
fmt.Printf("\n Events on %s: %d total\n", now.Format("2006-01-02"), len(todayEvents))
294+
295+
// ── 5. Hourly digest for today ────────────────────────────────────────────
296+
fmt.Println("\n Hourly digest (today only):")
297+
fmt.Println(" ─────────────────────────────────────────────────────────")
298+
buckets := bucketByHour(todayEvents)
299+
for _, b := range buckets {
300+
fmt.Printf(" %s UTC — %d event(s)\n",
301+
b.Hour.Format("15:04"), len(b.Events))
302+
for _, e := range b.Events {
303+
fmt.Printf(" • [%s] %s %s by %s\n",
304+
e.Severity, e.Action, e.Resource, e.ActorID)
305+
}
306+
}
307+
308+
// ── 6. Retention window: events older than 90 days can be archived ─────────
309+
fmt.Println("\n Retention check (90-day archive threshold):")
310+
fmt.Println(" ─────────────────────────────────────────────────────────")
311+
archiveThreshold := timefy.BeginOfDay(timefy.AddDay(now, -90))
312+
for _, e := range events {
313+
if timefy.New(e.OccurredAt).IsBefore(archiveThreshold) {
314+
fmt.Printf(" ARCHIVE: %s occurred %s\n",
315+
e.ID, e.OccurredAt.Format("2006-01-02"))
316+
}
317+
}
318+
fmt.Println(" (no events in this demo are older than 90 days)")
319+
320+
// ── 7. Calendar context for the CISO weekly report ────────────────────────
321+
fmt.Println("\n CISO weekly report context:")
322+
fmt.Println(" ─────────────────────────────────────────────────────────")
323+
tx := timefy.New(now)
324+
fmt.Printf(" Current week : %s → %s\n",
325+
tx.BeginningOfWeek().Format("2006-01-02 Mon"),
326+
tx.EndOfWeek().Format("2006-01-02 Mon"))
327+
fmt.Printf(" Current quarter : Q%d %s → %s\n",
328+
tx.Quarter(),
329+
tx.BeginningOfQuarter().Format("2006-01-02"),
330+
tx.EndOfQuarter().Format("2006-01-02"))
331+
fmt.Printf(" Year-to-date : %s → %s\n",
332+
tx.BeginningOfYear().Format("2006-01-02"),
333+
now.Format("2006-01-02"))
334+
}

0 commit comments

Comments
 (0)