Files
geo/server/internal/ops/app/compliance.go
T
root 745cdd79cf feat(compliance): add content compliance detection across tenant, ops, and clients
Implements the Revision 8/9 compliance design: tenant + ops backends, ops-web
content-safety pages, admin-web editor/publish gate integration, desktop publish
block surfacing, and supporting migrations / shared types / config plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:48:14 +08:00

1461 lines
49 KiB
Go

package app
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"unicode/utf8"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/zap"
sharedcompliance "github.com/geo-platform/tenant-api/internal/shared/compliance"
"github.com/geo-platform/tenant-api/internal/shared/response"
)
type ComplianceService struct {
pool *pgxpool.Pool
audits *AuditService
logger *zap.Logger
}
func NewComplianceService(pool *pgxpool.Pool, audits *AuditService, logger *zap.Logger) *ComplianceService {
return &ComplianceService{pool: pool, audits: audits, logger: logger}
}
type ComplianceDictionary struct {
ID int64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
PlatformScope string `json:"platform_scope"`
ApplicablePlatforms []string `json:"applicable_platforms"`
DefaultLevel string `json:"default_level"`
Enabled bool `json:"enabled"`
Version int `json:"version"`
Description *string `json:"description,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TermCount int `json:"term_count"`
}
type UpsertComplianceDictionaryRequest struct {
Code string `json:"code"`
Name string `json:"name"`
PlatformScope string `json:"platform_scope"`
ApplicablePlatforms []string `json:"applicable_platforms"`
DefaultLevel string `json:"default_level"`
Enabled *bool `json:"enabled"`
Description *string `json:"description"`
}
type ComplianceDictionaryTerm struct {
ID int64 `json:"id"`
DictionaryID int64 `json:"dictionary_id"`
MatchType string `json:"match_type"`
Pattern string `json:"pattern"`
LevelOverride *string `json:"level_override,omitempty"`
Hint *string `json:"hint,omitempty"`
ReferenceLaw *string `json:"reference_law,omitempty"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateComplianceDictionaryTermRequest struct {
MatchType string `json:"match_type"`
Pattern string `json:"pattern"`
LevelOverride *string `json:"level_override"`
Hint *string `json:"hint"`
ReferenceLaw *string `json:"reference_law"`
Enabled *bool `json:"enabled"`
}
type BatchImportComplianceTermsRequest struct {
Terms []CreateComplianceDictionaryTermRequest `json:"terms"`
}
type CompliancePolicy struct {
ID int64 `json:"id"`
MasterEnabled *bool `json:"master_enabled,omitempty"`
EnforcementMode string `json:"enforcement_mode"`
EnabledDictionaries []int64 `json:"enabled_dictionaries"`
LLMJudgeEnabled bool `json:"llm_judge_enabled"`
LLMCategories []string `json:"llm_categories"`
Revision int64 `json:"revision"`
UpdatedAt time.Time `json:"updated_at"`
}
type UpdateCompliancePolicyRequest struct {
MasterEnabled *bool `json:"master_enabled"`
EnforcementMode string `json:"enforcement_mode"`
EnabledDictionaries []int64 `json:"enabled_dictionaries"`
LLMJudgeEnabled *bool `json:"llm_judge_enabled"`
LLMCategories []string `json:"llm_categories"`
}
type ComplianceManualReviewListRequest struct {
Status string
TenantID *int64
Page int
PageSize int
}
type ComplianceManualReviewListResponse struct {
Items []sharedcompliance.ManualReview `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type ComplianceRecordListRequest struct {
TenantID *int64
ArticleID *int64
Decision string
Page int
PageSize int
}
type ComplianceRecordListResponse struct {
Items []sharedcompliance.CheckResult `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type ComplianceStats struct {
CheckCount int64 `json:"check_count"`
BlockedCount int64 `json:"blocked_count"`
NeedsAckCount int64 `json:"needs_ack_count"`
PassedCount int64 `json:"passed_count"`
AckCount int64 `json:"ack_count"`
ManualPendingCount int64 `json:"manual_pending_count"`
ManualApprovedCount int64 `json:"manual_approved_count"`
ManualRejectedCount int64 `json:"manual_rejected_count"`
ByLevel map[string]int64 `json:"by_level"`
BySource map[string]int64 `json:"by_source"`
}
func (s *ComplianceService) ListDictionaries(ctx context.Context) ([]ComplianceDictionary, error) {
rows, err := s.pool.Query(ctx, `
SELECT
d.id,
d.code,
d.name,
d.platform_scope,
COALESCE(d.applicable_platforms, '{}'),
d.default_level,
d.enabled,
d.version,
d.description,
d.created_at,
d.updated_at,
COUNT(t.id)::int AS term_count
FROM ops.compliance_dictionaries d
LEFT JOIN ops.compliance_dictionary_terms t ON t.dictionary_id = d.id
GROUP BY d.id
ORDER BY d.id DESC
`)
if err != nil {
return nil, response.ErrInternal(52001, "ops_compliance_dictionary_query_failed", "failed to list compliance dictionaries")
}
defer rows.Close()
out := make([]ComplianceDictionary, 0)
for rows.Next() {
item, scanErr := scanComplianceDictionary(rows)
if scanErr != nil {
return nil, response.ErrInternal(52001, "ops_compliance_dictionary_query_failed", "failed to scan compliance dictionary")
}
out = append(out, item)
}
return out, rows.Err()
}
func (s *ComplianceService) CreateDictionary(ctx context.Context, req UpsertComplianceDictionaryRequest) (*ComplianceDictionary, error) {
normalized, err := normalizeDictionaryInput(req, false)
if err != nil {
return nil, err
}
actor := ActorFromContext(ctx)
var actorID *int64
if actor != nil && actor.OperatorID > 0 {
actorID = &actor.OperatorID
}
var row ComplianceDictionary
var description pgtype.Text
err = s.pool.QueryRow(ctx, `
INSERT INTO ops.compliance_dictionaries (
code,
name,
platform_scope,
applicable_platforms,
default_level,
enabled,
description,
created_by,
updated_by
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$8)
RETURNING id, code, name, platform_scope, COALESCE(applicable_platforms, '{}'), default_level, enabled, version, description, created_at, updated_at, 0
`, normalized.Code, normalized.Name, normalized.PlatformScope, normalized.ApplicablePlatforms, normalized.DefaultLevel, normalized.Enabled, normalized.Description, actorID).Scan(
&row.ID,
&row.Code,
&row.Name,
&row.PlatformScope,
&row.ApplicablePlatforms,
&row.DefaultLevel,
&row.Enabled,
&row.Version,
&description,
&row.CreatedAt,
&row.UpdatedAt,
&row.TermCount,
)
if err != nil {
if isOpsUniqueViolation(err) {
return nil, response.ErrConflict(40971, "compliance_dictionary_code_exists", "dictionary code already exists")
}
return nil, response.ErrInternal(52002, "ops_compliance_dictionary_create_failed", "failed to create compliance dictionary")
}
if description.Valid {
row.Description = &description.String
}
s.audit(ctx, "compliance.dictionary.create", "compliance_dictionary", row.ID, map[string]any{"code": row.Code})
return &row, nil
}
func (s *ComplianceService) UpdateDictionary(ctx context.Context, id int64, req UpsertComplianceDictionaryRequest) (*ComplianceDictionary, error) {
normalized, err := normalizeDictionaryInput(req, true)
if err != nil {
return nil, err
}
actor := ActorFromContext(ctx)
var actorID *int64
if actor != nil && actor.OperatorID > 0 {
actorID = &actor.OperatorID
}
var row ComplianceDictionary
var description pgtype.Text
err = s.pool.QueryRow(ctx, `
UPDATE ops.compliance_dictionaries
SET name = $2,
platform_scope = $3,
applicable_platforms = $4,
default_level = $5,
enabled = $6,
description = $7,
updated_by = $8,
version = version + 1,
updated_at = NOW()
WHERE id = $1
RETURNING id, code, name, platform_scope, COALESCE(applicable_platforms, '{}'), default_level, enabled, version, description, created_at, updated_at,
(SELECT COUNT(*)::int FROM ops.compliance_dictionary_terms t WHERE t.dictionary_id = ops.compliance_dictionaries.id)
`, id, normalized.Name, normalized.PlatformScope, normalized.ApplicablePlatforms, normalized.DefaultLevel, normalized.Enabled, normalized.Description, actorID).Scan(
&row.ID,
&row.Code,
&row.Name,
&row.PlatformScope,
&row.ApplicablePlatforms,
&row.DefaultLevel,
&row.Enabled,
&row.Version,
&description,
&row.CreatedAt,
&row.UpdatedAt,
&row.TermCount,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrNotFound(40471, "compliance_dictionary_not_found", "compliance dictionary not found")
}
return nil, response.ErrInternal(52003, "ops_compliance_dictionary_update_failed", "failed to update compliance dictionary")
}
if description.Valid {
row.Description = &description.String
}
s.audit(ctx, "compliance.dictionary.update", "compliance_dictionary", row.ID, map[string]any{"code": row.Code})
return &row, nil
}
func (s *ComplianceService) DeleteDictionary(ctx context.Context, id int64) error {
tag, err := s.pool.Exec(ctx, `DELETE FROM ops.compliance_dictionaries WHERE id = $1`, id)
if err != nil {
return response.ErrInternal(52004, "ops_compliance_dictionary_delete_failed", "failed to delete compliance dictionary")
}
if tag.RowsAffected() == 0 {
return response.ErrNotFound(40471, "compliance_dictionary_not_found", "compliance dictionary not found")
}
s.audit(ctx, "compliance.dictionary.delete", "compliance_dictionary", id, nil)
return nil
}
func (s *ComplianceService) ListTerms(ctx context.Context, dictionaryID int64) ([]ComplianceDictionaryTerm, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, dictionary_id, match_type, pattern, level_override, hint, reference_law, enabled, created_at, updated_at
FROM ops.compliance_dictionary_terms
WHERE dictionary_id = $1
ORDER BY id DESC
`, dictionaryID)
if err != nil {
return nil, response.ErrInternal(52005, "ops_compliance_terms_query_failed", "failed to list compliance terms")
}
defer rows.Close()
out := make([]ComplianceDictionaryTerm, 0)
for rows.Next() {
item, scanErr := scanComplianceDictionaryTerm(rows)
if scanErr != nil {
return nil, response.ErrInternal(52005, "ops_compliance_terms_query_failed", "failed to scan compliance term")
}
out = append(out, item)
}
return out, rows.Err()
}
func (s *ComplianceService) CreateTerm(ctx context.Context, dictionaryID int64, req CreateComplianceDictionaryTermRequest) (*ComplianceDictionaryTerm, error) {
normalized, err := normalizeTermInput(req)
if err != nil {
return nil, err
}
var item ComplianceDictionaryTerm
err = s.pool.QueryRow(ctx, `
INSERT INTO ops.compliance_dictionary_terms (
dictionary_id,
match_type,
pattern,
level_override,
hint,
reference_law,
enabled
)
VALUES ($1,$2,$3,$4,$5,$6,$7)
RETURNING id, dictionary_id, match_type, pattern, level_override, hint, reference_law, enabled, created_at, updated_at
`, dictionaryID, normalized.MatchType, normalized.Pattern, normalized.LevelOverride, normalized.Hint, normalized.ReferenceLaw, normalized.Enabled).Scan(
&item.ID,
&item.DictionaryID,
&item.MatchType,
&item.Pattern,
new(pgtype.Text),
new(pgtype.Text),
new(pgtype.Text),
&item.Enabled,
&item.CreatedAt,
&item.UpdatedAt,
)
if err != nil {
if isOpsForeignKeyViolation(err) {
return nil, response.ErrNotFound(40471, "compliance_dictionary_not_found", "compliance dictionary not found")
}
return nil, response.ErrInternal(52006, "ops_compliance_term_create_failed", "failed to create compliance term")
}
item, err = s.getTerm(ctx, item.ID)
if err != nil {
return nil, err
}
_ = s.bumpDictionaryVersion(ctx, dictionaryID)
s.audit(ctx, "compliance.term.create", "compliance_dictionary", dictionaryID, map[string]any{"term_id": item.ID})
return &item, nil
}
func (s *ComplianceService) BatchImportTerms(ctx context.Context, dictionaryID int64, req BatchImportComplianceTermsRequest) ([]ComplianceDictionaryTerm, error) {
if len(req.Terms) == 0 {
return nil, response.ErrBadRequest(40001, "invalid_params", "terms is required")
}
if len(req.Terms) > 1000 {
return nil, response.ErrBadRequest(40001, "invalid_params", "terms cannot exceed 1000 rows")
}
out := make([]ComplianceDictionaryTerm, 0, len(req.Terms))
for _, item := range req.Terms {
created, err := s.CreateTerm(ctx, dictionaryID, item)
if err != nil {
return nil, err
}
out = append(out, *created)
}
return out, nil
}
func (s *ComplianceService) DeleteTerm(ctx context.Context, dictionaryID, termID int64) error {
tag, err := s.pool.Exec(ctx, `
DELETE FROM ops.compliance_dictionary_terms
WHERE id = $1 AND dictionary_id = $2
`, termID, dictionaryID)
if err != nil {
return response.ErrInternal(52007, "ops_compliance_term_delete_failed", "failed to delete compliance term")
}
if tag.RowsAffected() == 0 {
return response.ErrNotFound(40472, "compliance_term_not_found", "compliance term not found")
}
_ = s.bumpDictionaryVersion(ctx, dictionaryID)
s.audit(ctx, "compliance.term.delete", "compliance_dictionary", dictionaryID, map[string]any{"term_id": termID})
return nil
}
func (s *ComplianceService) PublishDictionary(ctx context.Context, dictionaryID int64) (*ComplianceDictionary, error) {
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, response.ErrInternal(52008, "ops_compliance_dictionary_publish_failed", "failed to begin dictionary publish")
}
defer tx.Rollback(ctx)
dict, err := s.loadDictionaryForUpdate(ctx, tx, dictionaryID)
if err != nil {
return nil, err
}
dict.Version++
if _, err := tx.Exec(ctx, `
UPDATE ops.compliance_dictionaries
SET version = $2, updated_at = NOW()
WHERE id = $1
`, dictionaryID, dict.Version); err != nil {
return nil, response.ErrInternal(52008, "ops_compliance_dictionary_publish_failed", "failed to bump dictionary version")
}
if err := s.insertDictionaryVersionSnapshot(ctx, tx, dictionaryID, dict.Version); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, response.ErrInternal(52008, "ops_compliance_dictionary_publish_failed", "failed to commit dictionary publish")
}
s.audit(ctx, "compliance.dictionary.publish", "compliance_dictionary", dictionaryID, map[string]any{"version": dict.Version})
return s.GetDictionary(ctx, dictionaryID)
}
func (s *ComplianceService) RollbackDictionary(ctx context.Context, dictionaryID int64, version int) (*ComplianceDictionary, error) {
if version <= 0 {
return nil, response.ErrBadRequest(40001, "invalid_params", "version must be positive")
}
var snapshot []byte
err := s.pool.QueryRow(ctx, `
SELECT snapshot
FROM ops.compliance_dictionary_versions
WHERE dictionary_id = $1 AND version = $2
`, dictionaryID, version).Scan(&snapshot)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrNotFound(40473, "compliance_dictionary_version_not_found", "dictionary version not found")
}
return nil, response.ErrInternal(52009, "ops_compliance_dictionary_rollback_failed", "failed to load dictionary snapshot")
}
var parsed struct {
Name string `json:"name"`
PlatformScope string `json:"platform_scope"`
ApplicablePlatforms []string `json:"applicable_platforms"`
DefaultLevel string `json:"default_level"`
Terms []struct {
MatchType string `json:"match_type"`
Pattern string `json:"pattern"`
LevelOverride *string `json:"level_override"`
Hint *string `json:"hint"`
ReferenceLaw *string `json:"reference_law"`
Enabled bool `json:"enabled"`
} `json:"terms"`
}
_ = json.Unmarshal(snapshot, &parsed)
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, response.ErrInternal(52009, "ops_compliance_dictionary_rollback_failed", "failed to begin dictionary rollback")
}
defer tx.Rollback(ctx)
tag, err := tx.Exec(ctx, `
UPDATE ops.compliance_dictionaries
SET name = COALESCE(NULLIF($2, ''), name),
platform_scope = COALESCE(NULLIF($3, ''), platform_scope),
applicable_platforms = $4,
default_level = COALESCE(NULLIF($5, ''), default_level),
version = version + 1,
updated_at = NOW()
WHERE id = $1
`, dictionaryID, parsed.Name, parsed.PlatformScope, normalizedPlatformArray(parsed.ApplicablePlatforms, parsed.PlatformScope), parsed.DefaultLevel)
if err != nil {
return nil, response.ErrInternal(52009, "ops_compliance_dictionary_rollback_failed", "failed to rollback dictionary")
}
if tag.RowsAffected() == 0 {
return nil, response.ErrNotFound(40471, "compliance_dictionary_not_found", "compliance dictionary not found")
}
if _, err := tx.Exec(ctx, `DELETE FROM ops.compliance_dictionary_terms WHERE dictionary_id = $1`, dictionaryID); err != nil {
return nil, response.ErrInternal(52009, "ops_compliance_dictionary_rollback_failed", "failed to clear dictionary terms")
}
for _, term := range parsed.Terms {
pattern := strings.TrimSpace(term.Pattern)
if pattern == "" {
continue
}
matchType := normalizeTermMatchType(term.MatchType)
if matchType == "" {
matchType = "exact"
}
if _, err := tx.Exec(ctx, `
INSERT INTO ops.compliance_dictionary_terms (
dictionary_id,
match_type,
pattern,
level_override,
hint,
reference_law,
enabled
)
VALUES ($1,$2,$3,$4,$5,$6,$7)
`, dictionaryID, matchType, pattern, nullableStringPointer(term.LevelOverride), nullableStringPointer(term.Hint), nullableStringPointer(term.ReferenceLaw), term.Enabled); err != nil {
return nil, response.ErrInternal(52009, "ops_compliance_dictionary_rollback_failed", "failed to restore dictionary terms")
}
}
if err := tx.Commit(ctx); err != nil {
return nil, response.ErrInternal(52009, "ops_compliance_dictionary_rollback_failed", "failed to commit dictionary rollback")
}
s.audit(ctx, "compliance.dictionary.rollback", "compliance_dictionary", dictionaryID, map[string]any{"version": version})
return s.GetDictionary(ctx, dictionaryID)
}
func (s *ComplianceService) GetDictionary(ctx context.Context, id int64) (*ComplianceDictionary, error) {
var item ComplianceDictionary
var description pgtype.Text
err := s.pool.QueryRow(ctx, `
SELECT
d.id,
d.code,
d.name,
d.platform_scope,
COALESCE(d.applicable_platforms, '{}'),
d.default_level,
d.enabled,
d.version,
d.description,
d.created_at,
d.updated_at,
(SELECT COUNT(*)::int FROM ops.compliance_dictionary_terms t WHERE t.dictionary_id = d.id)
FROM ops.compliance_dictionaries d
WHERE d.id = $1
`, id).Scan(
&item.ID,
&item.Code,
&item.Name,
&item.PlatformScope,
&item.ApplicablePlatforms,
&item.DefaultLevel,
&item.Enabled,
&item.Version,
&description,
&item.CreatedAt,
&item.UpdatedAt,
&item.TermCount,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrNotFound(40471, "compliance_dictionary_not_found", "compliance dictionary not found")
}
return nil, response.ErrInternal(52001, "ops_compliance_dictionary_query_failed", "failed to load compliance dictionary")
}
if description.Valid {
item.Description = &description.String
}
return &item, nil
}
func (s *ComplianceService) GetGlobalPolicy(ctx context.Context) (*CompliancePolicy, error) {
policy, err := s.getPolicy(ctx)
if err != nil {
return nil, err
}
return policy, nil
}
func (s *ComplianceService) UpdateGlobalPolicy(ctx context.Context, req UpdateCompliancePolicyRequest) (*CompliancePolicy, error) {
policy, err := s.updatePolicy(ctx, req)
if err != nil {
return nil, err
}
s.audit(ctx, "compliance.policy.global.update", "compliance_policy", policy.ID, nil)
return policy, nil
}
func (s *ComplianceService) SetGlobalMasterSwitch(ctx context.Context, enabled bool) (*CompliancePolicy, error) {
req := UpdateCompliancePolicyRequest{MasterEnabled: &enabled}
policy, err := s.updatePolicy(ctx, req)
if err != nil {
return nil, err
}
s.audit(ctx, "compliance.policy.global.master_switch", "compliance_policy", policy.ID, map[string]any{"enabled": enabled})
return policy, nil
}
func (s *ComplianceService) ListManualReviews(ctx context.Context, req ComplianceManualReviewListRequest) (*ComplianceManualReviewListResponse, error) {
req.Page, req.PageSize = normalizePage(req.Page, req.PageSize)
where, args := manualReviewWhere(req.Status, req.TenantID)
var total int64
if err := s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM compliance_manual_reviews mr WHERE "+where, args...).Scan(&total); err != nil {
return nil, response.ErrInternal(52020, "ops_compliance_manual_reviews_query_failed", "failed to count manual reviews")
}
args = append(args, req.PageSize, (req.Page-1)*req.PageSize)
limitArg := len(args) - 1
offsetArg := len(args)
rows, err := s.pool.Query(ctx, fmt.Sprintf(`
SELECT
mr.id,
mr.tenant_id,
mr.article_id,
mr.article_version_id,
mr.original_check_record_id,
mr.requested_by,
mr.requested_at,
mr.request_note,
mr.status,
mr.cancelled_by,
mr.cancelled_at,
mr.cancel_reason,
mr.reviewed_by,
mr.reviewed_at,
mr.decision_reason,
mr.original_violation_summary,
mr.original_target_platforms,
av.title,
u.name,
t.name
FROM compliance_manual_reviews mr
LEFT JOIN article_versions av ON av.id = mr.article_version_id
LEFT JOIN users u ON u.id = mr.requested_by
LEFT JOIN tenants t ON t.id = mr.tenant_id
WHERE %s
ORDER BY mr.requested_at DESC, mr.id DESC
LIMIT $%d OFFSET $%d
`, where, limitArg, offsetArg), args...)
if err != nil {
return nil, response.ErrInternal(52020, "ops_compliance_manual_reviews_query_failed", "failed to list manual reviews")
}
defer rows.Close()
items := make([]sharedcompliance.ManualReview, 0)
for rows.Next() {
item, scanErr := scanManualReviewForOps(rows, false)
if scanErr != nil {
return nil, response.ErrInternal(52020, "ops_compliance_manual_reviews_query_failed", "failed to scan manual review")
}
items = append(items, item)
}
return &ComplianceManualReviewListResponse{Items: items, Total: total, Page: req.Page, PageSize: req.PageSize}, rows.Err()
}
func (s *ComplianceService) GetManualReview(ctx context.Context, id int64) (*sharedcompliance.ManualReview, error) {
review, err := s.loadManualReview(ctx, id, true)
if err != nil {
return nil, err
}
return review, nil
}
func (s *ComplianceService) ApproveManualReview(ctx context.Context, id int64, reason string) (*sharedcompliance.ManualReview, error) {
return s.decideManualReview(ctx, id, "approved", reason)
}
func (s *ComplianceService) RejectManualReview(ctx context.Context, id int64, reason string) (*sharedcompliance.ManualReview, error) {
return s.decideManualReview(ctx, id, "rejected", reason)
}
func (s *ComplianceService) ListRecords(ctx context.Context, req ComplianceRecordListRequest) (*ComplianceRecordListResponse, error) {
req.Page, req.PageSize = normalizePage(req.Page, req.PageSize)
where, args := complianceRecordWhere(req)
var total int64
if err := s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM compliance_check_records WHERE "+where, args...).Scan(&total); err != nil {
return nil, response.ErrInternal(52030, "ops_compliance_records_query_failed", "failed to count compliance records")
}
args = append(args, req.PageSize, (req.Page-1)*req.PageSize)
limitArg := len(args) - 1
offsetArg := len(args)
rows, err := s.pool.Query(ctx, fmt.Sprintf(`
SELECT
id,
tenant_id,
article_id,
article_version_id,
target_platforms,
content_hash,
policy_fingerprint,
enforcement_mode,
highest_level,
passed,
hit_count,
stored_hit_count,
truncated,
manual_review_status,
manual_review_id,
checked_at,
duration_ms
FROM compliance_check_records
WHERE %s
ORDER BY checked_at DESC, id DESC
LIMIT $%d OFFSET $%d
`, where, limitArg, offsetArg), args...)
if err != nil {
return nil, response.ErrInternal(52030, "ops_compliance_records_query_failed", "failed to list compliance records")
}
defer rows.Close()
items := make([]sharedcompliance.CheckResult, 0)
for rows.Next() {
record, scanErr := scanCheckRecordForOps(rows)
if scanErr != nil {
return nil, response.ErrInternal(52030, "ops_compliance_records_query_failed", "failed to scan compliance record")
}
violations, err := s.loadRecordViolations(ctx, record.ID, record.TenantID)
if err != nil {
return nil, err
}
items = append(items, *checkResultFromOpsRecord(record, violations))
}
return &ComplianceRecordListResponse{Items: items, Total: total, Page: req.Page, PageSize: req.PageSize}, rows.Err()
}
func (s *ComplianceService) Stats(ctx context.Context) (*ComplianceStats, error) {
stats := &ComplianceStats{
ByLevel: map[string]int64{},
BySource: map[string]int64{},
}
if err := s.pool.QueryRow(ctx, `
SELECT
COUNT(*)::bigint,
COUNT(*) FILTER (WHERE enforcement_mode = 'mandatory' AND highest_level = 'block')::bigint,
COUNT(*) FILTER (WHERE hit_count > 0 AND NOT (enforcement_mode = 'mandatory' AND highest_level = 'block'))::bigint,
COUNT(*) FILTER (WHERE hit_count = 0)::bigint
FROM compliance_check_records
`).Scan(&stats.CheckCount, &stats.BlockedCount, &stats.NeedsAckCount, &stats.PassedCount); err != nil {
return nil, response.ErrInternal(52040, "ops_compliance_stats_query_failed", "failed to load compliance stats")
}
_ = s.pool.QueryRow(ctx, `SELECT COUNT(*)::bigint FROM compliance_ack_records`).Scan(&stats.AckCount)
_ = s.pool.QueryRow(ctx, `
SELECT
COUNT(*) FILTER (WHERE status = 'pending')::bigint,
COUNT(*) FILTER (WHERE status = 'approved')::bigint,
COUNT(*) FILTER (WHERE status = 'rejected')::bigint
FROM compliance_manual_reviews
`).Scan(&stats.ManualPendingCount, &stats.ManualApprovedCount, &stats.ManualRejectedCount)
levelRows, err := s.pool.Query(ctx, `SELECT level, COUNT(*)::bigint FROM compliance_violations GROUP BY level`)
if err == nil {
defer levelRows.Close()
for levelRows.Next() {
var level string
var count int64
if scanErr := levelRows.Scan(&level, &count); scanErr == nil {
stats.ByLevel[level] = count
}
}
}
sourceRows, err := s.pool.Query(ctx, `SELECT source, COUNT(*)::bigint FROM compliance_violations GROUP BY source`)
if err == nil {
defer sourceRows.Close()
for sourceRows.Next() {
var source string
var count int64
if scanErr := sourceRows.Scan(&source, &count); scanErr == nil {
stats.BySource[source] = count
}
}
}
return stats, nil
}
func (s *ComplianceService) decideManualReview(ctx context.Context, id int64, status, reason string) (*sharedcompliance.ManualReview, error) {
reason = strings.TrimSpace(reason)
if reason == "" {
return nil, response.ErrBadRequest(40001, "invalid_params", "reason is required")
}
if utf8.RuneCountInString(reason) > 1000 {
return nil, response.ErrBadRequest(40001, "invalid_params", "reason must be 1000 characters or less")
}
actor := ActorFromContext(ctx)
if actor == nil || actor.OperatorID <= 0 {
return nil, response.ErrUnauthorized(40101, "missing_operator", "operator context is required")
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, response.ErrInternal(52021, "ops_compliance_manual_review_decision_failed", "failed to begin manual review decision")
}
defer tx.Rollback(ctx)
var reviewID int64
var articleID int64
var versionID int64
err = tx.QueryRow(ctx, `
UPDATE compliance_manual_reviews
SET status = $2,
reviewed_by = $3,
reviewed_at = NOW(),
decision_reason = $4
WHERE id = $1 AND status = 'pending'
RETURNING id, article_id, article_version_id
`, id, status, actor.OperatorID, reason).Scan(&reviewID, &articleID, &versionID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrConflict(41010, "compliance_manual_review_not_cancellable", "manual review is not pending")
}
return nil, response.ErrInternal(52021, "ops_compliance_manual_review_decision_failed", "failed to update manual review")
}
if _, err := tx.Exec(ctx, `
UPDATE article_versions
SET manual_review_status = $1,
manual_review_id = $2,
manual_review_updated_at = NOW()
WHERE id = $3 AND article_id = $4
`, status, reviewID, versionID, articleID); err != nil {
return nil, response.ErrInternal(52021, "ops_compliance_manual_review_decision_failed", "failed to stamp article version")
}
if err := tx.Commit(ctx); err != nil {
return nil, response.ErrInternal(52021, "ops_compliance_manual_review_decision_failed", "failed to commit manual review decision")
}
s.audit(ctx, "compliance.manual_review."+status, "compliance_manual_review", id, map[string]any{"reason": reason})
return s.loadManualReview(ctx, id, true)
}
func (s *ComplianceService) getPolicy(ctx context.Context) (*CompliancePolicy, error) {
var policy CompliancePolicy
if err := scanPolicy(s.pool.QueryRow(ctx, `
SELECT id, master_enabled, enforcement_mode, enabled_dictionaries, llm_judge_enabled, llm_categories, revision, updated_at
FROM ops.compliance_policies
WHERE id = 1
LIMIT 1
`), &policy); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrNotFound(40474, "compliance_policy_not_found", "compliance policy not found")
}
return nil, response.ErrInternal(52010, "ops_compliance_policy_query_failed", "failed to load compliance policy")
}
return &policy, nil
}
func (s *ComplianceService) updatePolicy(ctx context.Context, req UpdateCompliancePolicyRequest) (*CompliancePolicy, error) {
existing, _ := s.getPolicy(ctx)
mode := normalizeComplianceMode(req.EnforcementMode)
if mode == "" {
if existing != nil && existing.EnforcementMode != "" {
mode = existing.EnforcementMode
} else {
mode = "mandatory"
}
}
llmEnabled := false
if req.LLMJudgeEnabled != nil {
llmEnabled = *req.LLMJudgeEnabled
} else if existing != nil {
llmEnabled = existing.LLMJudgeEnabled
}
masterEnabled := req.MasterEnabled
if masterEnabled == nil {
if existing != nil {
masterEnabled = existing.MasterEnabled
}
if masterEnabled == nil {
defaultMaster := true
masterEnabled = &defaultMaster
}
}
enabledDictionaries := req.EnabledDictionaries
if enabledDictionaries == nil && existing != nil {
enabledDictionaries = existing.EnabledDictionaries
}
llmCategoriesValue := req.LLMCategories
if llmCategoriesValue == nil && existing != nil {
llmCategoriesValue = existing.LLMCategories
}
enabledDicts, _ := json.Marshal(enabledDictionaries)
llmCategories, _ := json.Marshal(llmCategoriesValue)
actor := ActorFromContext(ctx)
var actorID *int64
if actor != nil && actor.OperatorID > 0 {
actorID = &actor.OperatorID
}
var policy CompliancePolicy
if err := scanPolicy(s.pool.QueryRow(ctx, `
INSERT INTO ops.compliance_policies (
id, master_enabled, enforcement_mode, enabled_dictionaries, llm_judge_enabled, llm_categories, updated_by
)
VALUES (1, $1, $2, $3, $4, $5, $6)
ON CONFLICT (id)
DO UPDATE SET
master_enabled = EXCLUDED.master_enabled,
enforcement_mode = EXCLUDED.enforcement_mode,
enabled_dictionaries = EXCLUDED.enabled_dictionaries,
llm_judge_enabled = EXCLUDED.llm_judge_enabled,
llm_categories = EXCLUDED.llm_categories,
updated_by = EXCLUDED.updated_by,
revision = ops.compliance_policies.revision + 1,
updated_at = NOW()
RETURNING id, master_enabled, enforcement_mode, enabled_dictionaries, llm_judge_enabled, llm_categories, revision, updated_at
`, masterEnabled, mode, enabledDicts, llmEnabled, llmCategories, actorID), &policy); err != nil {
return nil, response.ErrInternal(52011, "ops_compliance_policy_update_failed", "failed to update compliance policy")
}
return &policy, nil
}
func (s *ComplianceService) loadManualReview(ctx context.Context, id int64, includePlaintext bool) (*sharedcompliance.ManualReview, error) {
query := `
SELECT
mr.id,
mr.tenant_id,
mr.article_id,
mr.article_version_id,
mr.original_check_record_id,
mr.requested_by,
mr.requested_at,
mr.request_note,
mr.status,
mr.cancelled_by,
mr.cancelled_at,
mr.cancel_reason,
mr.reviewed_by,
mr.reviewed_at,
mr.decision_reason,
mr.original_violation_summary,
mr.original_target_platforms,
av.title,
u.name,
t.name
`
if includePlaintext {
query += `, av.plaintext_snapshot`
}
query += `
FROM compliance_manual_reviews mr
LEFT JOIN article_versions av ON av.id = mr.article_version_id
LEFT JOIN users u ON u.id = mr.requested_by
LEFT JOIN tenants t ON t.id = mr.tenant_id
WHERE mr.id = $1
`
row := s.pool.QueryRow(ctx, query, id)
review, err := scanManualReviewForOps(row, includePlaintext)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrNotFound(40475, "compliance_manual_review_not_found", "manual review not found")
}
return nil, response.ErrInternal(52020, "ops_compliance_manual_reviews_query_failed", "failed to load manual review")
}
return &review, nil
}
func (s *ComplianceService) loadRecordViolations(ctx context.Context, recordID, tenantID int64) ([]sharedcompliance.Violation, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, record_id, platform_code, source, dictionary_code, dictionary_name, term_pattern, matched_text, start_offset, end_offset, dom_path, level, hint, reference_law
FROM compliance_violations
WHERE record_id = $1 AND tenant_id = $2
ORDER BY id
`, recordID, tenantID)
if err != nil {
return nil, response.ErrInternal(52031, "ops_compliance_violations_query_failed", "failed to load compliance violations")
}
defer rows.Close()
out := make([]sharedcompliance.Violation, 0)
for rows.Next() {
item, scanErr := scanViolationForOps(rows)
if scanErr != nil {
return nil, response.ErrInternal(52031, "ops_compliance_violations_query_failed", "failed to scan compliance violation")
}
out = append(out, item)
}
return out, rows.Err()
}
func (s *ComplianceService) bumpDictionaryVersion(ctx context.Context, dictionaryID int64) error {
_, err := s.pool.Exec(ctx, `
UPDATE ops.compliance_dictionaries
SET version = version + 1, updated_at = NOW()
WHERE id = $1
`, dictionaryID)
return err
}
func (s *ComplianceService) loadDictionaryForUpdate(ctx context.Context, tx pgx.Tx, id int64) (*ComplianceDictionary, error) {
var item ComplianceDictionary
err := tx.QueryRow(ctx, `
SELECT id, code, name, platform_scope, COALESCE(applicable_platforms, '{}'), default_level, enabled, version, description, created_at, updated_at, 0
FROM ops.compliance_dictionaries
WHERE id = $1
FOR UPDATE
`, id).Scan(&item.ID, &item.Code, &item.Name, &item.PlatformScope, &item.ApplicablePlatforms, &item.DefaultLevel, &item.Enabled, &item.Version, new(pgtype.Text), &item.CreatedAt, &item.UpdatedAt, &item.TermCount)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrNotFound(40471, "compliance_dictionary_not_found", "compliance dictionary not found")
}
return nil, response.ErrInternal(52001, "ops_compliance_dictionary_query_failed", "failed to load compliance dictionary")
}
return &item, nil
}
func (s *ComplianceService) insertDictionaryVersionSnapshot(ctx context.Context, tx pgx.Tx, dictionaryID int64, version int) error {
var snapshot []byte
err := tx.QueryRow(ctx, `
SELECT jsonb_build_object(
'code', d.code,
'name', d.name,
'platform_scope', d.platform_scope,
'applicable_platforms', COALESCE(to_jsonb(d.applicable_platforms), '[]'::jsonb),
'default_level', d.default_level,
'terms', COALESCE((
SELECT jsonb_agg(jsonb_build_object(
'id', t.id,
'match_type', t.match_type,
'pattern', t.pattern,
'level_override', t.level_override,
'hint', t.hint,
'reference_law', t.reference_law,
'enabled', t.enabled
) ORDER BY t.id)
FROM ops.compliance_dictionary_terms t
WHERE t.dictionary_id = d.id
), '[]'::jsonb)
)
FROM ops.compliance_dictionaries d
WHERE d.id = $1
`, dictionaryID).Scan(&snapshot)
if err != nil {
return response.ErrInternal(52008, "ops_compliance_dictionary_publish_failed", "failed to build dictionary snapshot")
}
actor := ActorFromContext(ctx)
var actorID *int64
if actor != nil && actor.OperatorID > 0 {
actorID = &actor.OperatorID
}
_, err = tx.Exec(ctx, `
INSERT INTO ops.compliance_dictionary_versions (dictionary_id, version, snapshot, published_by)
VALUES ($1,$2,$3,$4)
ON CONFLICT (dictionary_id, version) DO NOTHING
`, dictionaryID, version, snapshot, actorID)
if err != nil {
return response.ErrInternal(52008, "ops_compliance_dictionary_publish_failed", "failed to save dictionary snapshot")
}
return nil
}
func (s *ComplianceService) getTerm(ctx context.Context, termID int64) (ComplianceDictionaryTerm, error) {
row := s.pool.QueryRow(ctx, `
SELECT id, dictionary_id, match_type, pattern, level_override, hint, reference_law, enabled, created_at, updated_at
FROM ops.compliance_dictionary_terms
WHERE id = $1
`, termID)
item, err := scanComplianceDictionaryTerm(row)
if err != nil {
return item, response.ErrInternal(52005, "ops_compliance_terms_query_failed", "failed to load compliance term")
}
return item, nil
}
func (s *ComplianceService) audit(ctx context.Context, action, targetType string, targetID int64, metadata map[string]any) {
if s == nil || s.audits == nil {
return
}
actor := ActorFromContext(ctx)
if actor == nil {
return
}
if err := s.audits.Append(ctx, actor.audit(action, targetType, targetID, metadata)); err != nil && s.logger != nil {
s.logger.Warn("ops compliance audit failed", zap.String("action", action), zap.Error(err))
}
}
type complianceDictionaryScanner interface{ Scan(dest ...any) error }
func scanComplianceDictionary(scanner complianceDictionaryScanner) (ComplianceDictionary, error) {
var item ComplianceDictionary
var description pgtype.Text
if err := scanner.Scan(&item.ID, &item.Code, &item.Name, &item.PlatformScope, &item.ApplicablePlatforms, &item.DefaultLevel, &item.Enabled, &item.Version, &description, &item.CreatedAt, &item.UpdatedAt, &item.TermCount); err != nil {
return item, err
}
if description.Valid {
item.Description = &description.String
}
return item, nil
}
func scanComplianceDictionaryTerm(scanner complianceDictionaryScanner) (ComplianceDictionaryTerm, error) {
var item ComplianceDictionaryTerm
var level pgtype.Text
var hint pgtype.Text
var referenceLaw pgtype.Text
if err := scanner.Scan(&item.ID, &item.DictionaryID, &item.MatchType, &item.Pattern, &level, &hint, &referenceLaw, &item.Enabled, &item.CreatedAt, &item.UpdatedAt); err != nil {
return item, err
}
if level.Valid {
item.LevelOverride = &level.String
}
if hint.Valid {
item.Hint = &hint.String
}
if referenceLaw.Valid {
item.ReferenceLaw = &referenceLaw.String
}
return item, nil
}
func scanPolicy(scanner complianceDictionaryScanner, policy *CompliancePolicy) error {
var master pgtype.Bool
var enabledRaw []byte
var llmCategoriesRaw []byte
if err := scanner.Scan(&policy.ID, &master, &policy.EnforcementMode, &enabledRaw, &policy.LLMJudgeEnabled, &llmCategoriesRaw, &policy.Revision, &policy.UpdatedAt); err != nil {
return err
}
if master.Valid {
policy.MasterEnabled = &master.Bool
}
_ = json.Unmarshal(enabledRaw, &policy.EnabledDictionaries)
_ = json.Unmarshal(llmCategoriesRaw, &policy.LLMCategories)
if policy.EnabledDictionaries == nil {
policy.EnabledDictionaries = []int64{}
}
if policy.LLMCategories == nil {
policy.LLMCategories = []string{}
}
return nil
}
func scanManualReviewForOps(scanner complianceDictionaryScanner, includePlaintext bool) (sharedcompliance.ManualReview, error) {
var item sharedcompliance.ManualReview
var checkID pgtype.Int8
var requestNote pgtype.Text
var cancelledBy pgtype.Int8
var cancelledAt pgtype.Timestamptz
var cancelReason pgtype.Text
var reviewedBy pgtype.Int8
var reviewedAt pgtype.Timestamptz
var decisionReason pgtype.Text
var summary []byte
var platforms []string
var title pgtype.Text
var applicant pgtype.Text
var tenant pgtype.Text
dest := []any{
&item.ID, &item.TenantID, &item.ArticleID, &item.ArticleVersionID, &checkID, &item.RequestedBy, &item.RequestedAt, &requestNote, &item.Status, &cancelledBy, &cancelledAt, &cancelReason, &reviewedBy, &reviewedAt, &decisionReason, &summary, &platforms, &title, &applicant, &tenant,
}
var plaintext pgtype.Text
if includePlaintext {
dest = append(dest, &plaintext)
}
if err := scanner.Scan(dest...); err != nil {
return item, err
}
if checkID.Valid {
item.CheckRecordID = &checkID.Int64
}
if requestNote.Valid {
item.RequestNote = &requestNote.String
}
if cancelledBy.Valid {
item.CancelledBy = &cancelledBy.Int64
}
if cancelledAt.Valid {
item.CancelledAt = &cancelledAt.Time
}
if cancelReason.Valid {
item.CancelReason = &cancelReason.String
}
if reviewedBy.Valid {
item.ReviewedBy = &reviewedBy.Int64
}
if reviewedAt.Valid {
item.ReviewedAt = &reviewedAt.Time
}
if decisionReason.Valid {
item.DecisionReason = &decisionReason.String
}
_ = json.Unmarshal(summary, &item.ViolationSummary)
item.OriginalPlatforms = platforms
if title.Valid {
item.ArticleTitle = &title.String
}
if applicant.Valid {
item.ApplicantName = &applicant.String
}
if tenant.Valid {
item.TenantName = &tenant.String
}
if includePlaintext && plaintext.Valid {
item.ArticlePlaintext = &plaintext.String
}
return item, nil
}
type opsCheckRecordRow struct {
ID int64
TenantID int64
ArticleID int64
ArticleVersionID int64
TargetPlatforms []string
ContentHash string
PolicyFingerprint string
EnforcementMode string
HighestLevel *string
Passed bool
HitCount int
StoredHitCount int
Truncated bool
ManualReviewStatus sharedcompliance.ManualReviewStatus
ManualReviewID *int64
CheckedAt time.Time
DurationMS int
}
func scanCheckRecordForOps(scanner complianceDictionaryScanner) (opsCheckRecordRow, error) {
var record opsCheckRecordRow
var articleID pgtype.Int8
var versionID pgtype.Int8
var highest pgtype.Text
var manualReviewID pgtype.Int8
var manualStatus string
if err := scanner.Scan(&record.ID, &record.TenantID, &articleID, &versionID, &record.TargetPlatforms, &record.ContentHash, &record.PolicyFingerprint, &record.EnforcementMode, &highest, &record.Passed, &record.HitCount, &record.StoredHitCount, &record.Truncated, &manualStatus, &manualReviewID, &record.CheckedAt, &record.DurationMS); err != nil {
return record, err
}
if articleID.Valid {
record.ArticleID = articleID.Int64
}
if versionID.Valid {
record.ArticleVersionID = versionID.Int64
}
if highest.Valid {
record.HighestLevel = &highest.String
}
if manualReviewID.Valid {
record.ManualReviewID = &manualReviewID.Int64
}
record.ManualReviewStatus = sharedcompliance.ManualReviewStatus(manualStatus)
return record, nil
}
func scanViolationForOps(scanner complianceDictionaryScanner) (sharedcompliance.Violation, error) {
var item sharedcompliance.Violation
var platform pgtype.Text
var dictCode pgtype.Text
var dictName pgtype.Text
var termPattern pgtype.Text
var domPath pgtype.Text
var hint pgtype.Text
var ref pgtype.Text
if err := scanner.Scan(&item.ID, &item.RecordID, &platform, &item.Source, &dictCode, &dictName, &termPattern, &item.MatchedText, &item.StartOffset, &item.EndOffset, &domPath, &item.Level, &hint, &ref); err != nil {
return item, err
}
if platform.Valid {
item.PlatformCode = &platform.String
}
if dictCode.Valid {
item.DictionaryCode = &dictCode.String
}
if dictName.Valid {
item.DictionaryName = &dictName.String
}
if termPattern.Valid {
item.TermPattern = &termPattern.String
}
if domPath.Valid {
item.DomPath = &domPath.String
}
if hint.Valid {
item.Hint = &hint.String
}
if ref.Valid {
item.ReferenceLaw = &ref.String
}
return item, nil
}
func checkResultFromOpsRecord(record opsCheckRecordRow, violations []sharedcompliance.Violation) *sharedcompliance.CheckResult {
decision := sharedcompliance.GateDecisionPass
if record.HitCount > 0 {
decision = sharedcompliance.GateDecisionNeedsAck
if record.EnforcementMode == "mandatory" && record.HighestLevel != nil && *record.HighestLevel == "block" {
decision = sharedcompliance.GateDecisionBlock
}
}
return &sharedcompliance.CheckResult{
RecordID: record.ID,
Decision: decision,
Passed: record.Passed,
HighestLevel: record.HighestLevel,
Violations: violations,
HitCount: record.HitCount,
StoredHitCount: record.StoredHitCount,
Truncated: record.Truncated,
EnforcementMode: record.EnforcementMode,
ContentHash: record.ContentHash,
PolicyFingerprint: record.PolicyFingerprint,
TargetPlatforms: record.TargetPlatforms,
ManualReviewStatus: record.ManualReviewStatus,
ManualReviewID: record.ManualReviewID,
CheckedAt: record.CheckedAt,
DurationMS: record.DurationMS,
}
}
func normalizeDictionaryInput(req UpsertComplianceDictionaryRequest, updating bool) (UpsertComplianceDictionaryRequest, error) {
req.Code = strings.TrimSpace(req.Code)
req.Name = strings.TrimSpace(req.Name)
req.PlatformScope = strings.ToLower(strings.TrimSpace(req.PlatformScope))
req.DefaultLevel = normalizeComplianceLevel(req.DefaultLevel)
if !updating && req.Code == "" {
return req, response.ErrBadRequest(40001, "invalid_params", "code is required")
}
if req.Name == "" {
return req, response.ErrBadRequest(40001, "invalid_params", "name is required")
}
if req.PlatformScope == "" {
req.PlatformScope = "all"
}
if req.PlatformScope != "all" && req.PlatformScope != "specific" {
return req, response.ErrBadRequest(40001, "invalid_params", "platform_scope must be all or specific")
}
req.ApplicablePlatforms = normalizedPlatformArray(req.ApplicablePlatforms, req.PlatformScope)
if req.PlatformScope == "specific" && len(req.ApplicablePlatforms) == 0 {
return req, response.ErrBadRequest(40001, "invalid_params", "applicable_platforms is required for specific scope")
}
if req.DefaultLevel == "" {
req.DefaultLevel = "block"
}
enabled := true
if req.Enabled != nil {
enabled = *req.Enabled
}
req.Enabled = &enabled
if req.Description != nil {
value := strings.TrimSpace(*req.Description)
req.Description = &value
}
return req, nil
}
func normalizeTermInput(req CreateComplianceDictionaryTermRequest) (CreateComplianceDictionaryTermRequest, error) {
req.MatchType = normalizeTermMatchType(req.MatchType)
if req.MatchType == "" {
req.MatchType = "exact"
}
req.Pattern = strings.TrimSpace(req.Pattern)
if req.Pattern == "" {
return req, response.ErrBadRequest(40001, "invalid_params", "pattern is required")
}
if req.LevelOverride != nil {
level := normalizeComplianceLevel(*req.LevelOverride)
if level == "" {
return req, response.ErrBadRequest(40001, "invalid_params", "level_override is invalid")
}
req.LevelOverride = &level
}
enabled := true
if req.Enabled != nil {
enabled = *req.Enabled
}
req.Enabled = &enabled
return req, nil
}
func normalizeTermMatchType(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
switch value {
case "", "exact":
return "exact"
case "regex":
return value
default:
return ""
}
}
func normalizeComplianceLevel(level string) string {
level = strings.ToLower(strings.TrimSpace(level))
switch level {
case "block", "high", "medium", "info":
return level
default:
return ""
}
}
func normalizeComplianceMode(mode string) string {
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "mandatory", "advisory", "disabled":
return mode
default:
return ""
}
}
func normalizedPlatformArray(platforms []string, scope string) []string {
if strings.EqualFold(strings.TrimSpace(scope), "all") {
return nil
}
seen := map[string]struct{}{}
out := make([]string, 0, len(platforms))
for _, platform := range platforms {
platform = strings.TrimSpace(platform)
if platform == "" {
continue
}
if _, ok := seen[platform]; ok {
continue
}
seen[platform] = struct{}{}
out = append(out, platform)
}
return out
}
func nullableStringPointer(value *string) *string {
if value == nil {
return nil
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" {
return nil
}
return &trimmed
}
func manualReviewWhere(status string, tenantID *int64) (string, []any) {
where := "1=1"
args := []any{}
status = strings.ToLower(strings.TrimSpace(status))
if status != "" {
args = append(args, status)
where += fmt.Sprintf(" AND mr.status = $%d", len(args))
}
if tenantID != nil && *tenantID > 0 {
args = append(args, *tenantID)
where += fmt.Sprintf(" AND mr.tenant_id = $%d", len(args))
}
return where, args
}
func complianceRecordWhere(req ComplianceRecordListRequest) (string, []any) {
where := "1=1"
args := []any{}
if req.TenantID != nil && *req.TenantID > 0 {
args = append(args, *req.TenantID)
where += fmt.Sprintf(" AND tenant_id = $%d", len(args))
}
if req.ArticleID != nil && *req.ArticleID > 0 {
args = append(args, *req.ArticleID)
where += fmt.Sprintf(" AND article_id = $%d", len(args))
}
switch strings.TrimSpace(req.Decision) {
case "pass":
where += " AND hit_count = 0"
case "block":
where += " AND enforcement_mode = 'mandatory' AND highest_level = 'block'"
case "needs_ack":
where += " AND hit_count > 0 AND NOT (enforcement_mode = 'mandatory' AND highest_level = 'block')"
}
return where, args
}
func normalizePage(page, size int) (int, int) {
if page <= 0 {
page = 1
}
if size <= 0 || size > 200 {
size = 50
}
return page, size
}
func isOpsUniqueViolation(err error) bool {
var pgErr *pgconn.PgError
return errors.As(err, &pgErr) && pgErr.Code == "23505"
}
func isOpsForeignKeyViolation(err error) bool {
var pgErr *pgconn.PgError
return errors.As(err, &pgErr) && pgErr.Code == "23503"
}