chore: add copyright headers, CI tests, and sanitize gitignore

This commit is contained in:
DmitrL-dev 2026-03-31 22:13:34 +10:00
parent 5cbb3d89d3
commit d1f844235e
325 changed files with 2267 additions and 902 deletions

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (
@ -62,13 +66,13 @@ func SeedDemoTenant(userStore *UserStore, tenantStore *TenantStore, socRepo doms
// 2. Create demo tenant (demo plan 1000 events max)
demoTenant := &Tenant{
ID: DemoTenantID,
Name: "SYNTREX Demo",
Slug: "demo",
PlanID: "demo",
OwnerUserID: demoUser.ID,
Active: true,
CreatedAt: time.Now(),
ID: DemoTenantID,
Name: "SYNTREX Demo",
Slug: "demo",
PlanID: "demo",
OwnerUserID: demoUser.ID,
Active: true,
CreatedAt: time.Now(),
MonthResetAt: monthStart(time.Now().AddDate(0, 1, 0)),
}

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (
@ -31,7 +35,9 @@ func HandleLogin(store *UserStore, secret []byte) http.HandlerFunc {
email := req.Email
if email == "" {
// Try legacy format
var legacy struct{ Username string `json:"username"` }
var legacy struct {
Username string `json:"username"`
}
email = legacy.Username
}

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
// Package auth provides JWT authentication for the SOC HTTP API.
// Uses HMAC-SHA256 (HS256) with configurable secret.
// Zero external dependencies — pure Go stdlib.
@ -16,21 +20,21 @@ import (
// Standard JWT errors.
var (
ErrInvalidToken = errors.New("auth: invalid token")
ErrExpiredToken = errors.New("auth: token expired")
ErrInvalidSecret = errors.New("auth: secret too short (min 32 bytes)")
ErrWrongTokenType = errors.New("auth: wrong token type")
ErrInvalidToken = errors.New("auth: invalid token")
ErrExpiredToken = errors.New("auth: token expired")
ErrInvalidSecret = errors.New("auth: secret too short (min 32 bytes)")
ErrWrongTokenType = errors.New("auth: wrong token type")
)
// Claims represents JWT payload.
type Claims struct {
Sub string `json:"sub"` // Subject (username or user ID)
Role string `json:"role"` // RBAC role: admin, operator, analyst, viewer
TenantID string `json:"tenant_id,omitempty"` // Multi-tenant isolation
Sub string `json:"sub"` // Subject (username or user ID)
Role string `json:"role"` // RBAC role: admin, operator, analyst, viewer
TenantID string `json:"tenant_id,omitempty"` // Multi-tenant isolation
TokenType string `json:"token_type,omitempty"` // "access" or "refresh"
Exp int64 `json:"exp"` // Expiration (Unix timestamp)
Iat int64 `json:"iat"` // Issued at
Iss string `json:"iss,omitempty"` // Issuer
Exp int64 `json:"exp"` // Expiration (Unix timestamp)
Iat int64 `json:"iat"` // Issued at
Iss string `json:"iss,omitempty"` // Issuer
}
// IsExpired returns true if the token has expired.

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (
@ -37,9 +41,9 @@ func TestSign_Verify_RoundTrip(t *testing.T) {
func TestVerify_ExpiredToken(t *testing.T) {
token, _ := Sign(Claims{
Sub: "user",
Sub: "user",
Role: "viewer",
Exp: time.Now().Add(-time.Hour).Unix(),
Exp: time.Now().Add(-time.Hour).Unix(),
}, testSecret)
_, err := Verify(token, testSecret)
@ -50,9 +54,9 @@ func TestVerify_ExpiredToken(t *testing.T) {
func TestVerify_InvalidSignature(t *testing.T) {
token, _ := Sign(Claims{
Sub: "user",
Sub: "user",
Role: "viewer",
Exp: time.Now().Add(time.Hour).Unix(),
Exp: time.Now().Add(time.Hour).Unix(),
}, testSecret)
wrongSecret := []byte("wrong-secret-that-is-also-32-bytes-x")

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (
@ -13,7 +17,7 @@ const claimsKey ctxKey = "jwt_claims"
// JWTMiddleware validates Bearer tokens on protected routes.
type JWTMiddleware struct {
secret []byte
secret []byte
// PublicPaths are exempt from auth (e.g., /health, /api/auth/login).
PublicPaths map[string]bool
}
@ -23,23 +27,23 @@ func NewJWTMiddleware(secret []byte) *JWTMiddleware {
return &JWTMiddleware{
secret: secret,
PublicPaths: map[string]bool{
"/health": true,
"/healthz": true,
"/readyz": true,
"/metrics": true,
"/api/auth/login": true,
"/api/auth/logout": true,
"/api/auth/refresh": true,
"/api/auth/register": true,
"/api/auth/verify": true,
"/api/auth/plans": true,
"/api/auth/demo": true,
"/api/v1/scan": true, // public demo scanner
"/api/v1/usage": true, // public usage/quota check
"/api/v1/soc/events": true, // sensor ingest (auth via RBAC API key when enabled)
"/health": true,
"/healthz": true,
"/readyz": true,
"/metrics": true,
"/api/auth/login": true,
"/api/auth/logout": true,
"/api/auth/refresh": true,
"/api/auth/register": true,
"/api/auth/verify": true,
"/api/auth/plans": true,
"/api/auth/demo": true,
"/api/v1/scan": true, // public demo scanner
"/api/v1/usage": true, // public usage/quota check
"/api/v1/soc/events": true, // sensor ingest (auth via RBAC API key when enabled)
"/api/soc/events/stream": true, // SSE uses query param auth
"/api/soc/stream": true, // SSE live feed (EventSource can't send headers)
"/api/soc/ws": true, // WebSocket-style SSE push
"/api/soc/stream": true, // SSE live feed (EventSource can't send headers)
"/api/soc/ws": true, // WebSocket-style SSE push
},
}
}

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (
@ -250,8 +254,8 @@ func HandleGetTenant(tenantStore *TenantStore) http.HandlerFunc {
"plan": plan,
"usage": map[string]interface{}{
"events_this_month": tenant.EventsThisMonth,
"events_limit": plan.MaxEventsMonth,
"usage_percent": usagePercent(tenant.EventsThisMonth, plan.MaxEventsMonth),
"events_limit": plan.MaxEventsMonth,
"usage_percent": usagePercent(tenant.EventsThisMonth, plan.MaxEventsMonth),
},
})
}
@ -339,13 +343,13 @@ func HandleBillingStatus(tenantStore *TenantStore) http.HandlerFunc {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"plan": plan,
"plan": plan,
"payment_customer_id": tenant.PaymentCustomerID,
"payment_sub_id": tenant.PaymentSubID,
"events_used": tenant.EventsThisMonth,
"events_limit": plan.MaxEventsMonth,
"usage_percent": usagePercent(tenant.EventsThisMonth, plan.MaxEventsMonth),
"next_reset": tenant.MonthResetAt,
"events_used": tenant.EventsThisMonth,
"events_limit": plan.MaxEventsMonth,
"usage_percent": usagePercent(tenant.EventsThisMonth, plan.MaxEventsMonth),
"next_reset": tenant.MonthResetAt,
})
}
}
@ -419,7 +423,7 @@ func HandleListTenants(tenantStore *TenantStore) http.HandlerFunc {
}
tenants := tenantStore.ListTenants()
type tenantResp struct {
ID string `json:"id"`
Name string `json:"name"`
@ -427,7 +431,7 @@ func HandleListTenants(tenantStore *TenantStore) http.HandlerFunc {
PlanID string `json:"plan_id"`
Active bool `json:"active"`
}
res := make([]tenantResp, len(tenants))
for i, t := range tenants {
res[i] = tenantResp{
@ -471,16 +475,16 @@ func HandleImpersonateTenant(tenantStore *TenantStore, jwtSecret []byte) http.Ha
// Issue new token with updated TenantID
accessClaims := Claims{
Sub: claims.Sub,
Role: claims.Role, // Preserves superadmin explicitly
Role: claims.Role, // Preserves superadmin explicitly
TenantID: req.TenantID, // The impersonated tenant ID
TokenType: "access",
Exp: time.Now().Add(15 * time.Minute).Unix(),
}
refreshClaims := Claims{
Sub: claims.Sub,
Role: claims.Role,
TenantID: req.TenantID,
TenantID: req.TenantID,
TokenType: "refresh",
Exp: time.Now().Add(7 * 24 * time.Hour).Unix(),
}
@ -521,8 +525,8 @@ func HandleImpersonateTenant(tenantStore *TenantStore, jwtSecret []byte) http.Ha
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"tenant_id": req.TenantID,
"status": "success",
"tenant_id": req.TenantID,
"csrf_token": csrfToken,
})
}

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (
@ -22,17 +26,17 @@ type Plan struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
MaxUsers int `json:"max_users"`
MaxEventsMonth int `json:"max_events_month"` // SOC event ingestion quota (-1=unlimited)
MaxEventsMonth int `json:"max_events_month"` // SOC event ingestion quota (-1=unlimited)
MaxIncidents int `json:"max_incidents"`
MaxSensors int `json:"max_sensors"`
MaxScansMonth int `json:"max_scans_month"` // /api/v1/scan quota (-1=unlimited, 0=none)
MaxScansMonth int `json:"max_scans_month"` // /api/v1/scan quota (-1=unlimited, 0=none)
RetentionDays int `json:"retention_days"`
SOCEnabled bool `json:"soc_enabled"` // SOC Dashboard access
SOCEnabled bool `json:"soc_enabled"` // SOC Dashboard access
SLAEnabled bool `json:"sla_enabled"`
SOAREnabled bool `json:"soar_enabled"`
ComplianceEnabled bool `json:"compliance_enabled"`
OnPremise bool `json:"on_premise"` // Enterprise: on-premise deployment
PriceMonthCents int `json:"price_month_cents"` // 0 = free, -1 = custom pricing
OnPremise bool `json:"on_premise"` // Enterprise: on-premise deployment
PriceMonthCents int `json:"price_month_cents"` // 0 = free, -1 = custom pricing
}
// DefaultPlans defines the standard pricing tiers (prices in RUB kopecks).
@ -40,64 +44,64 @@ var DefaultPlans = map[string]Plan{
"free": {
ID: "free", Name: "Free",
Description: "Scanner API — 1 000 сканов/мес, все 66 движков, без SOC Dashboard",
MaxUsers: 1, MaxEventsMonth: 1000, MaxIncidents: 100, MaxSensors: 1,
MaxUsers: 1, MaxEventsMonth: 1000, MaxIncidents: 100, MaxSensors: 1,
MaxScansMonth: 1000,
RetentionDays: 3,
SOCEnabled: false, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false,
SOCEnabled: false, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false,
PriceMonthCents: 0,
},
"demo": {
ID: "demo", Name: "Demo Sandbox",
Description: "Общая демо-песочница. Жёсткий лимит.",
MaxUsers: 10, MaxEventsMonth: 1000, MaxIncidents: 100, MaxSensors: 5,
MaxUsers: 10, MaxEventsMonth: 1000, MaxIncidents: 100, MaxSensors: 5,
MaxScansMonth: 1000,
RetentionDays: 1,
SOCEnabled: true, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false,
SOCEnabled: true, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false,
PriceMonthCents: 0,
},
"starter": {
ID: "starter", Name: "Starter",
Description: "AI-мониторинг: до 5 сенсоров, базовая корреляция и алерты",
MaxUsers: 10, MaxEventsMonth: 100000, MaxIncidents: 200, MaxSensors: 5,
MaxUsers: 10, MaxEventsMonth: 100000, MaxIncidents: 200, MaxSensors: 5,
MaxScansMonth: 100000,
RetentionDays: 30,
SOCEnabled: true, SLAEnabled: true, SOAREnabled: false, ComplianceEnabled: false,
SOCEnabled: true, SLAEnabled: true, SOAREnabled: false, ComplianceEnabled: false,
PriceMonthCents: 8990000, // 89 900 ₽/мес
},
"professional": {
ID: "professional", Name: "Professional",
Description: "Полный AI SOC: SOAR, compliance, расширенная аналитика",
MaxUsers: 50, MaxEventsMonth: 500000, MaxIncidents: 1000, MaxSensors: 25,
MaxUsers: 50, MaxEventsMonth: 500000, MaxIncidents: 1000, MaxSensors: 25,
MaxScansMonth: 500000,
RetentionDays: 90,
SOCEnabled: true, SLAEnabled: true, SOAREnabled: true, ComplianceEnabled: true,
SOCEnabled: true, SLAEnabled: true, SOAREnabled: true, ComplianceEnabled: true,
PriceMonthCents: 14990000, // 149 900 ₽/мес
},
"enterprise": {
ID: "enterprise", Name: "Enterprise",
Description: "On-premise / выделенный инстанс. Сертификация — на стороне заказчика",
MaxUsers: -1, MaxEventsMonth: -1, MaxIncidents: -1, MaxSensors: -1,
MaxUsers: -1, MaxEventsMonth: -1, MaxIncidents: -1, MaxSensors: -1,
MaxScansMonth: -1, // unlimited
RetentionDays: 365,
SOCEnabled: true, SLAEnabled: true, SOAREnabled: true, ComplianceEnabled: true,
OnPremise: true,
SOCEnabled: true, SLAEnabled: true, SOAREnabled: true, ComplianceEnabled: true,
OnPremise: true,
PriceMonthCents: -1, // по запросу
},
}
// Tenant represents an isolated organization in the multi-tenant system.
type Tenant struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
PlanID string `json:"plan_id"`
PaymentCustomerID string `json:"payment_customer_id,omitempty"`
PaymentSubID string `json:"payment_sub_id,omitempty"`
OwnerUserID string `json:"owner_user_id"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
EventsThisMonth int `json:"events_this_month"`
MonthResetAt time.Time `json:"month_reset_at"`
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
PlanID string `json:"plan_id"`
PaymentCustomerID string `json:"payment_customer_id,omitempty"`
PaymentSubID string `json:"payment_sub_id,omitempty"`
OwnerUserID string `json:"owner_user_id"`
Active bool `json:"active"`
CreatedAt time.Time `json:"created_at"`
EventsThisMonth int `json:"events_this_month"`
MonthResetAt time.Time `json:"month_reset_at"`
}
// GetPlan returns the tenant's plan configuration.

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (

View file

@ -1,3 +1,7 @@
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
package auth
import (
@ -67,7 +71,7 @@ func NewUserStore(db ...*sql.DB) *UserStore {
// Ensure default admin exists or is updated
adminPass := os.Getenv("SYNTREX_ADMIN_PASSWORD")
if adminPass == "" {
// If no env var, use a secure random password to prevent accidental exposure
// If no env var, use a secure random password to prevent accidental exposure
// if the database is clean, but do not override an existing admin's password.
b := make([]byte, 16)
rand.Read(b)
@ -408,12 +412,12 @@ func (s *UserStore) DeleteUser(id string) error {
// APIKey represents an API key for programmatic access.
type APIKey struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Role string `json:"role"`
KeyPrefix string `json:"key_prefix"` // first 8 chars for display
CreatedAt time.Time `json:"created_at"`
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Role string `json:"role"`
KeyPrefix string `json:"key_prefix"` // first 8 chars for display
CreatedAt time.Time `json:"created_at"`
LastUsed *time.Time `json:"last_used,omitempty"`
}