First release of open core
This commit is contained in:
127
pkg/sla/sla.go
Normal file
127
pkg/sla/sla.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package sla
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"epigas.gitea.cloud/RiskRancher/core/pkg/domain"
|
||||
)
|
||||
|
||||
// DefaultSLACalculator implements the SLACalculator interface
|
||||
type DefaultSLACalculator struct {
|
||||
Timezone string
|
||||
BusinessStart int
|
||||
BusinessEnd int
|
||||
Holidays map[string]bool
|
||||
}
|
||||
|
||||
// NewSLACalculator returns the interface
|
||||
func NewSLACalculator() domain.SLACalculator {
|
||||
return &DefaultSLACalculator{
|
||||
Timezone: "UTC",
|
||||
BusinessStart: 9,
|
||||
BusinessEnd: 17,
|
||||
Holidays: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateDueDate for the finding based on SLA
|
||||
func (c *DefaultSLACalculator) CalculateDueDate(severity string) *time.Time {
|
||||
var days int
|
||||
switch severity {
|
||||
case "Critical":
|
||||
days = 3
|
||||
case "High":
|
||||
days = 14
|
||||
case "Medium":
|
||||
days = 30
|
||||
case "Low":
|
||||
days = 90
|
||||
default:
|
||||
days = 30
|
||||
}
|
||||
|
||||
loc, err := time.LoadLocation(c.Timezone)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Invalid timezone '%s', falling back to UTC", c.Timezone)
|
||||
loc = time.UTC
|
||||
}
|
||||
|
||||
nowLocal := time.Now().In(loc)
|
||||
dueDate := c.AddBusinessDays(nowLocal, days)
|
||||
return &dueDate
|
||||
}
|
||||
|
||||
// AddBusinessDays for working days not weekends and some holidays
|
||||
func (c *DefaultSLACalculator) AddBusinessDays(start time.Time, businessDays int) time.Time {
|
||||
current := start
|
||||
added := 0
|
||||
for added < businessDays {
|
||||
current = current.AddDate(0, 0, 1)
|
||||
weekday := current.Weekday()
|
||||
dateStr := current.Format("2006-01-02")
|
||||
if weekday != time.Saturday && weekday != time.Sunday && !c.Holidays[dateStr] {
|
||||
added++
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
// CalculateTrueSLAHours based on the time of action for ticket
|
||||
func (c *DefaultSLACalculator) CalculateTrueSLAHours(ctx context.Context, ticketID int, store domain.Store) (float64, error) {
|
||||
appConfig, err := store.GetAppConfig(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ticket, err := store.GetTicketByID(ctx, ticketID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
end := time.Now()
|
||||
if ticket.PatchedAt != nil {
|
||||
end = *ticket.PatchedAt
|
||||
}
|
||||
|
||||
totalActiveBusinessHours := c.calculateBusinessHoursBetween(ticket.CreatedAt, end, appConfig)
|
||||
return totalActiveBusinessHours, nil
|
||||
}
|
||||
|
||||
// calculateBusinessHoursBetween calculates strict working hours between two timestamps
|
||||
func (c *DefaultSLACalculator) calculateBusinessHoursBetween(start, end time.Time, config domain.AppConfig) float64 {
|
||||
loc, _ := time.LoadLocation(config.Timezone)
|
||||
start = start.In(loc)
|
||||
end = end.In(loc)
|
||||
|
||||
if start.After(end) {
|
||||
return 0
|
||||
}
|
||||
|
||||
var activeHours float64
|
||||
current := start
|
||||
|
||||
for current.Before(end) {
|
||||
nextHour := current.Add(time.Hour)
|
||||
if nextHour.After(end) {
|
||||
nextHour = end
|
||||
}
|
||||
|
||||
weekday := current.Weekday()
|
||||
dateStr := current.Format("2006-01-02")
|
||||
hour := current.Hour()
|
||||
|
||||
isWeekend := weekday == time.Saturday || weekday == time.Sunday
|
||||
isHoliday := c.Holidays[dateStr]
|
||||
isBusinessHour := hour >= config.BusinessStart && hour < config.BusinessEnd
|
||||
|
||||
if !isWeekend && !isHoliday && isBusinessHour {
|
||||
activeHours += nextHour.Sub(current).Hours()
|
||||
}
|
||||
|
||||
current = nextHour
|
||||
}
|
||||
|
||||
return activeHours
|
||||
}
|
||||
116
pkg/sla/sla_test.go
Normal file
116
pkg/sla/sla_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package sla_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// GetSLAPolicy simulates the core engine function that fetches SLA rules
|
||||
func GetSLAPolicy(db *sql.DB, domain string, severity string) (daysToRemediate int, maxExtensions int, err error) {
|
||||
query := `SELECT days_to_remediate, max_extensions FROM sla_policies WHERE domain = ? AND severity = ?`
|
||||
err = db.QueryRow(query, domain, severity).Scan(&daysToRemediate, &maxExtensions)
|
||||
return daysToRemediate, maxExtensions, err
|
||||
}
|
||||
|
||||
// setupTestDB spins up an isolated, in-memory database for testing
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test database: %v", err)
|
||||
}
|
||||
|
||||
schema := `
|
||||
CREATE TABLE domains (name TEXT PRIMARY KEY);
|
||||
CREATE TABLE sla_policies (
|
||||
domain TEXT NOT NULL,
|
||||
severity TEXT NOT NULL,
|
||||
days_to_remediate INTEGER NOT NULL,
|
||||
max_extensions INTEGER NOT NULL DEFAULT 3,
|
||||
PRIMARY KEY (domain, severity)
|
||||
);
|
||||
INSERT INTO domains (name) VALUES ('Vulnerability'), ('Privacy'), ('Incident');
|
||||
INSERT INTO sla_policies (domain, severity, days_to_remediate, max_extensions) VALUES
|
||||
('Vulnerability', 'Critical', 14, 1),
|
||||
('Vulnerability', 'High', 30, 2),
|
||||
('Privacy', 'Critical', 3, 0),
|
||||
('Incident', 'Critical', 1, 0);
|
||||
`
|
||||
if _, err := db.Exec(schema); err != nil {
|
||||
t.Fatalf("Failed to execute test schema: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestSLAEngine(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
severity string
|
||||
expectDays int
|
||||
expectExtensions int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "VM Critical (Standard)",
|
||||
domain: "Vulnerability",
|
||||
severity: "Critical",
|
||||
expectDays: 14,
|
||||
expectExtensions: 1,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Privacy Critical (Strict 72-hour, No Extensions)",
|
||||
domain: "Privacy",
|
||||
severity: "Critical",
|
||||
expectDays: 3,
|
||||
expectExtensions: 0,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Incident Critical (24-hour, No Extensions)",
|
||||
domain: "Incident",
|
||||
severity: "Critical",
|
||||
expectDays: 1,
|
||||
expectExtensions: 0,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Unknown Domain (Should Fail)",
|
||||
domain: "PhysicalSecurity",
|
||||
severity: "Critical",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Unknown Severity (Should Fail)",
|
||||
domain: "Vulnerability",
|
||||
severity: "SuperCritical",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
days, extensions, err := GetSLAPolicy(db, tt.domain, tt.severity)
|
||||
|
||||
if (err != nil) != tt.expectError {
|
||||
t.Fatalf("expected error: %v, got: %v", tt.expectError, err)
|
||||
}
|
||||
|
||||
if tt.expectError {
|
||||
return
|
||||
}
|
||||
|
||||
if days != tt.expectDays {
|
||||
t.Errorf("expected %d days, got %d", tt.expectDays, days)
|
||||
}
|
||||
if extensions != tt.expectExtensions {
|
||||
t.Errorf("expected %d max extensions, got %d", tt.expectExtensions, extensions)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user