projection: begin the exact-path public epoch
The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally. Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
This commit is contained in:
commit
67e26be2d9
838 changed files with 157302 additions and 0 deletions
310
agent/internal/kernel/ebpf_consumer.go
Normal file
310
agent/internal/kernel/ebpf_consumer.go
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
package kernel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/Fimeg/RedFlag/agent/internal/config"
|
||||
"github.com/Fimeg/RedFlag/agent/internal/event"
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/ringbuf"
|
||||
"github.com/cilium/ebpf/rlimit"
|
||||
)
|
||||
|
||||
const (
|
||||
// MAGIC_NUMBER matches the eBPF program
|
||||
MAGIC_NUMBER = 0xDEADBEEF
|
||||
)
|
||||
|
||||
// EBPFConsumer reads events from the eBPF ring buffer and forwards them to the policy checker
|
||||
type EBPFConsumer struct {
|
||||
config *config.Config
|
||||
running bool
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
wgLock sync.Mutex
|
||||
rd *ringbuf.Reader
|
||||
elfPath string
|
||||
teeLogger *event.TeeLogger
|
||||
}
|
||||
|
||||
// NewEBPFConsumer creates a new eBPF ring buffer consumer
|
||||
func NewEBPFConsumer(cfg *config.Config) (*EBPFConsumer, error) {
|
||||
return NewEBPFConsumerWithLogger(cfg, nil)
|
||||
}
|
||||
|
||||
func NewEBPFConsumerWithLogger(cfg *config.Config, teeLogger *event.TeeLogger) (*EBPFConsumer, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &EBPFConsumer{
|
||||
config: cfg,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
elfPath: cfg.KernelEnforcement.RingBufferPath + "/pkg-gate.o",
|
||||
teeLogger: teeLogger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start begins consuming events from the eBPF ring buffer
|
||||
func (e *EBPFConsumer) Start() error {
|
||||
e.wgLock.Lock()
|
||||
if e.running {
|
||||
e.wgLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
e.running = true
|
||||
e.wgLock.Unlock()
|
||||
|
||||
log.Printf("[INFO] [kernel] [ebpf] consumer_started")
|
||||
|
||||
// Remove memlock limit for eBPF
|
||||
if err := rlimit.RemoveMemlock(); err != nil {
|
||||
return fmt.Errorf("failed to remove memlock: %w", err)
|
||||
}
|
||||
|
||||
// Load the eBPF collection from the ELF file
|
||||
spec, err := ebpf.LoadCollectionSpec(e.elfPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load eBPF collection spec: %w", err)
|
||||
}
|
||||
|
||||
// Find the ring buffer map by name
|
||||
_, ok := spec.Maps["rb_map"]
|
||||
if !ok {
|
||||
return fmt.Errorf("ring buffer map 'rb_map' not found in eBPF collection")
|
||||
}
|
||||
|
||||
// Load collection to get loaded maps
|
||||
coll, err := ebpf.NewCollection(spec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load eBPF collection: %w", err)
|
||||
}
|
||||
|
||||
// Access the loaded ring buffer map
|
||||
loadedMap := coll.Maps["rb_map"]
|
||||
if loadedMap == nil {
|
||||
coll.Close()
|
||||
return fmt.Errorf("ring buffer map 'rb_map' not found in loaded collection")
|
||||
}
|
||||
|
||||
// Create ring buffer reader
|
||||
rd, err := ringbuf.NewReader(loadedMap)
|
||||
if err != nil {
|
||||
coll.Close()
|
||||
return fmt.Errorf("failed to create ring buffer: %w", err)
|
||||
}
|
||||
|
||||
e.rd = rd
|
||||
e.elfPath = "" // Don't load again on stop
|
||||
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigChan
|
||||
log.Printf("[INFO] [kernel] [ebpf] shutdown_signal_received")
|
||||
e.cancel()
|
||||
e.wg.Wait()
|
||||
e.rd.Close()
|
||||
}()
|
||||
|
||||
e.wg.Add(1)
|
||||
go e.consumeEvents()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the consumer
|
||||
func (e *EBPFConsumer) Stop() error {
|
||||
e.cancel()
|
||||
e.wgLock.Lock()
|
||||
e.running = false
|
||||
e.wgLock.Unlock()
|
||||
|
||||
e.wg.Wait()
|
||||
if e.rd != nil {
|
||||
e.rd.Close()
|
||||
}
|
||||
|
||||
log.Printf("[INFO] [kernel] [ebpf] consumer_stopped")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// consumeEvents reads from the ring buffer and forwards events to the policy checker
|
||||
func (e *EBPFConsumer) consumeEvents() {
|
||||
defer e.wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Read event from ring buffer
|
||||
record, err := e.rd.Read()
|
||||
if err != nil {
|
||||
if errors.Is(err, ringbuf.ErrClosed) {
|
||||
return
|
||||
}
|
||||
e.teeLogger.Error("agent", "kernel", "kernel_enforcer", fmt.Sprintf("ringbuf_read_failed error=%v", err), map[string]interface{}{"error": err.Error()})
|
||||
continue
|
||||
}
|
||||
|
||||
e.processEvent(record.RawSample)
|
||||
}
|
||||
}
|
||||
|
||||
// processEvent handles a single eBPF event
|
||||
func (e *EBPFConsumer) processEvent(data []byte) {
|
||||
// Decode using the struct from eBPF (must match exactly)
|
||||
// Layout: timestamp(8) pid(4) pgid(4) uid(8) gid(8) parent_pid(4) comm(16) cmdline(256) parent_comm(16) magic(4) = 328 bytes
|
||||
event := &redflagEvent{}
|
||||
// Use unsafe to copy raw bytes into the struct
|
||||
bytess := (*[328]byte)(unsafe.Pointer(event))
|
||||
copy(bytess[0:8], data[0:8])
|
||||
copy(bytess[8:12], data[8:12])
|
||||
copy(bytess[12:16], data[12:16])
|
||||
copy(bytess[16:24], data[16:24])
|
||||
copy(bytess[24:32], data[24:32])
|
||||
copy(bytess[32:36], data[32:36])
|
||||
copy(bytess[36:52], data[36:52])
|
||||
copy(bytess[52:308], data[52:308])
|
||||
copy(bytess[308:324], data[308:324])
|
||||
copy(bytess[324:328], data[324:328])
|
||||
|
||||
// Validate magic number
|
||||
if event.Magic != MAGIC_NUMBER {
|
||||
e.teeLogger.Error("agent", "kernel", "kernel_enforcer", fmt.Sprintf("invalid_magic magic=0x%x expected=0x%x", event.Magic, MAGIC_NUMBER), map[string]interface{}{
|
||||
"magic": fmt.Sprintf("0x%x", event.Magic),
|
||||
"expected": fmt.Sprintf("0x%x", MAGIC_NUMBER),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Forward to rs-helper for policy decision
|
||||
decision, reason, err := e.checkPolicy(event)
|
||||
if err != nil {
|
||||
e.teeLogger.Error("agent", "kernel", "kernel_enforcer", fmt.Sprintf("policy_check_failed error=%v", err), map[string]interface{}{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if decision == "deny" {
|
||||
e.teeLogger.Error("agent", "kernel", "kernel_enforcer", fmt.Sprintf("execve_deny pid=%d comm=%s reason=%s", event.PID, event.Comm, reason), map[string]interface{}{
|
||||
"pid": event.PID,
|
||||
"comm": fmt.Sprintf("%s", event.Comm),
|
||||
"reason": reason,
|
||||
})
|
||||
} else {
|
||||
log.Printf("[INFO] [kernel] [ebpf] execve_allowed pid=%d comm=%s",
|
||||
event.PID, event.Comm)
|
||||
}
|
||||
}
|
||||
|
||||
// redflagEvent matches the eBPF struct redflag_event layout
|
||||
type redflagEvent struct {
|
||||
Timestamp uint64
|
||||
PID uint32
|
||||
PGID uint32
|
||||
UID uint64
|
||||
GID uint64
|
||||
ParentPID uint32
|
||||
Comm [16]byte
|
||||
Cmdline [256]byte
|
||||
ParentComm [16]byte
|
||||
Magic uint32
|
||||
}
|
||||
|
||||
// checkPolicy evaluates the policy for a package manager invocation
|
||||
func (e *EBPFConsumer) checkPolicy(event *redflagEvent) (string, string, error) {
|
||||
// Decode strings for policy check
|
||||
comm := *(*string)(unsafe.Pointer(&event.Comm))
|
||||
cmdline := *(*string)(unsafe.Pointer(&event.Cmdline))
|
||||
parentComm := *(*string)(unsafe.Pointer(&event.ParentComm))
|
||||
|
||||
log.Printf("[DEBUG] [kernel] [ebpf] policy_check comm=%s cmdline=%s parent=%s uid=%d",
|
||||
comm, cmdline, parentComm, event.UID)
|
||||
|
||||
// Check if this is a package manager command
|
||||
isPackageManager := isPackageManager(comm)
|
||||
if !isPackageManager {
|
||||
log.Printf("[INFO] [kernel] [ebpf] not_package_manager comm=%s", comm)
|
||||
return "allow", "not_package_manager", nil
|
||||
}
|
||||
|
||||
// TODO: Call rs-helper via Unix socket for policy decision
|
||||
// For now, return allow (agent should have rs-helper running)
|
||||
|
||||
return "allow", "policy_evaluation_pending", nil
|
||||
}
|
||||
|
||||
// isPackageManager checks if a command is a known package manager
|
||||
func isPackageManager(comm string) bool {
|
||||
pkgManagers := []string{"apt", "apt-get", "apt-cache", "dnf", "yum", "rpm-ostree",
|
||||
"npm", "pnpm", "bun", "pip", "pip3", "uv",
|
||||
"docker", "crun", "containerd"}
|
||||
|
||||
for _, pm := range pkgManagers {
|
||||
if comm == pm {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Event represents a typed intercept event for API use
|
||||
type Event struct {
|
||||
Timestamp time.Time
|
||||
PID uint32
|
||||
PGID uint32
|
||||
Comm string
|
||||
Cmdline string
|
||||
UID uint64
|
||||
GID uint64
|
||||
ParentPID uint32
|
||||
ParentComm string
|
||||
}
|
||||
|
||||
// FromBinary creates an Event from binary data (deprecated, use direct struct access)
|
||||
func FromBinary(data []byte) (*Event, error) {
|
||||
if len(data) < 328 {
|
||||
return nil, fmt.Errorf("data too short: %d", len(data))
|
||||
}
|
||||
|
||||
event := &redflagEvent{}
|
||||
// Use unsafe to copy raw bytes into the struct
|
||||
bytess := (*[328]byte)(unsafe.Pointer(event))
|
||||
copy(bytess[0:8], data[0:8])
|
||||
copy(bytess[8:12], data[8:12])
|
||||
copy(bytess[12:16], data[12:16])
|
||||
copy(bytess[16:24], data[16:24])
|
||||
copy(bytess[24:32], data[24:32])
|
||||
copy(bytess[32:36], data[32:36])
|
||||
copy(bytess[36:52], data[36:52])
|
||||
copy(bytess[52:308], data[52:308])
|
||||
copy(bytess[308:324], data[308:324])
|
||||
copy(bytess[324:328], data[324:328])
|
||||
|
||||
return &Event{
|
||||
Timestamp: time.Unix(0, int64(event.Timestamp)),
|
||||
PID: event.PID,
|
||||
PGID: event.PGID,
|
||||
Comm: string(bytes.TrimRight(event.Comm[:], "\x00")),
|
||||
Cmdline: string(bytes.TrimRight(event.Cmdline[:], "\x00")),
|
||||
UID: event.UID,
|
||||
GID: event.GID,
|
||||
ParentPID: event.ParentPID,
|
||||
ParentComm: string(bytes.TrimRight(event.ParentComm[:], "\x00")),
|
||||
}, nil
|
||||
}
|
||||
84
agent/internal/kernel/kernel.go
Normal file
84
agent/internal/kernel/kernel.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package kernel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/Fimeg/RedFlag/agent/internal/config"
|
||||
"github.com/Fimeg/RedFlag/agent/internal/event"
|
||||
)
|
||||
|
||||
type Enforcer interface {
|
||||
Start(ctx context.Context) error
|
||||
Stop() error
|
||||
GetPackageType() string
|
||||
}
|
||||
|
||||
type EnforcerFactory func(*config.Config, *event.TeeLogger) (Enforcer, error)
|
||||
|
||||
var enforcers = map[string]EnforcerFactory{}
|
||||
|
||||
func RegisterEnforcer(pkgType string, factory EnforcerFactory) {
|
||||
enforcers[pkgType] = factory
|
||||
}
|
||||
|
||||
func NewEnforcerWithLogger(cfg *config.Config, teeLogger *event.TeeLogger) (Enforcer, error) {
|
||||
if !cfg.KernelEnforcement.Enabled {
|
||||
return &noopEnforcer{}, nil
|
||||
}
|
||||
|
||||
// Determine package type based on platform
|
||||
pkgType := "linux"
|
||||
if cfg.OS.Type == "windows" {
|
||||
pkgType = "windows"
|
||||
}
|
||||
|
||||
if factory, ok := enforcers[pkgType]; ok {
|
||||
return factory(cfg, teeLogger)
|
||||
}
|
||||
|
||||
log.Printf("[WARNING] [kernel] unknown_package_type=%s", pkgType)
|
||||
return &noopEnforcer{}, nil
|
||||
}
|
||||
|
||||
type noopEnforcer struct{}
|
||||
|
||||
func (n *noopEnforcer) Start(ctx context.Context) error { return nil }
|
||||
func (n *noopEnforcer) Stop() error { return nil }
|
||||
func (n *noopEnforcer) GetPackageType() string { return "noop" }
|
||||
|
||||
type EBPFEnforcer struct {
|
||||
consumer *EBPFConsumer
|
||||
}
|
||||
|
||||
func NewEBPFEnforcer(cfg *config.Config) (*EBPFEnforcer, error) {
|
||||
return NewEBPFEnforcerWithLogger(cfg, nil)
|
||||
}
|
||||
|
||||
func NewEBPFEnforcerWithLogger(cfg *config.Config, teeLogger *event.TeeLogger) (*EBPFEnforcer, error) {
|
||||
consumer, err := NewEBPFConsumerWithLogger(cfg, teeLogger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EBPFEnforcer{consumer: consumer}, nil
|
||||
}
|
||||
|
||||
func (e *EBPFEnforcer) Start(ctx context.Context) error {
|
||||
return e.consumer.Start()
|
||||
}
|
||||
|
||||
func (e *EBPFEnforcer) Stop() error {
|
||||
return e.consumer.Stop()
|
||||
}
|
||||
|
||||
func (e *EBPFEnforcer) GetPackageType() string { return "linux" }
|
||||
|
||||
func init() {
|
||||
RegisterEnforcer("linux", func(cfg *config.Config, teeLogger *event.TeeLogger) (Enforcer, error) {
|
||||
consumer, err := NewEBPFConsumerWithLogger(cfg, teeLogger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EBPFEnforcer{consumer: consumer}, nil
|
||||
})
|
||||
}
|
||||
164
agent/internal/kernel/windows_wdac.go
Normal file
164
agent/internal/kernel/windows_wdac.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package kernel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/agent/internal/client"
|
||||
"github.com/Fimeg/RedFlag/agent/internal/config"
|
||||
)
|
||||
|
||||
// WDACPolicy handles Windows WDAC (Windows Defender Application Control) enforcement
|
||||
type WDACPolicy struct {
|
||||
client *client.Client
|
||||
config *config.Config
|
||||
policyName string
|
||||
policyHash string
|
||||
}
|
||||
|
||||
// NewWDACPolicy creates a new WDACPolicy enforcer
|
||||
func NewWDACPolicy(c *client.Client, cfg *config.Config) *WDACPolicy {
|
||||
return &WDACPolicy{
|
||||
client: c,
|
||||
config: cfg,
|
||||
policyName: "RedFlag-Package-Manager",
|
||||
}
|
||||
}
|
||||
|
||||
// GetPackageType returns the package type this enforcer handles
|
||||
func (w *WDACPolicy) GetPackageType() string {
|
||||
return "wdac"
|
||||
}
|
||||
|
||||
// IsAvailable checks if WDAC is available on this system
|
||||
func (w *WDACPolicy) IsAvailable() bool {
|
||||
// WDAC only available on Windows
|
||||
return true // Platform check done by caller
|
||||
}
|
||||
|
||||
// CheckPolicy evaluates if a package manager invocation is allowed
|
||||
func (w *WDACPolicy) CheckPolicy(packageType, packageName, packageVersion string) (bool, string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
log.Printf("[INFO] [kernel] [wdac] policy_check package=%s type=%s version=%s", packageName, packageType, packageVersion)
|
||||
|
||||
result, err := w.fetchPolicy(ctx)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [kernel] [wdac] policy_fetch_failed package=%s error=%v", packageName, err)
|
||||
return false, "policy_fetch_failed: " + err.Error(), nil
|
||||
}
|
||||
|
||||
if result == "" {
|
||||
return false, "no_policy", nil
|
||||
}
|
||||
|
||||
// Parse policy and check if package manager is allowed
|
||||
allowed, reason := w.evaluatePolicy(packageType, packageName, result)
|
||||
if !allowed {
|
||||
log.Printf("[ERROR] [kernel] [wdac] policy_denied package=%s reason=%s", packageName, reason)
|
||||
} else {
|
||||
log.Printf("[INFO] [kernel] [wdac] policy_allowed package=%s", packageName)
|
||||
}
|
||||
|
||||
return allowed, reason, nil
|
||||
}
|
||||
|
||||
// fetchPolicy retrieves the current WDAC policy from the server
|
||||
func (w *WDACPolicy) fetchPolicy(ctx context.Context) (string, error) {
|
||||
// TODO: Replace with actual WDAC COM API calls via go-ole
|
||||
// For now, fetch policy hash from server and validate against local policy
|
||||
|
||||
hash, err := w.client.GetWdacPolicyHash(w.config.AgentID, w.config.ServerURL)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] [kernel] [wdac] policy_hash_fetch_skipped error=%v", err)
|
||||
// For now, treat as no policy available (fail-closed)
|
||||
return "", fmt.Errorf("no_policy")
|
||||
}
|
||||
|
||||
if hash == "" {
|
||||
return "", fmt.Errorf("no WDAC policy available from server")
|
||||
}
|
||||
|
||||
if hash != w.policyHash {
|
||||
log.Printf("[WARNING] [kernel] [wdac] policy_hash_mismatch local=%s server=%s", w.policyHash, hash)
|
||||
// Policy needs update - return empty to trigger update
|
||||
return "", fmt.Errorf("policy_hash_mismatch")
|
||||
}
|
||||
|
||||
return w.policyHash, nil
|
||||
}
|
||||
|
||||
// evaluatePolicy checks if the package manager is allowed by the current policy
|
||||
func (w *WDACPolicy) evaluatePolicy(packageType, packageName, policyHash string) (bool, string) {
|
||||
// Allowed package managers (fail-closed: deny all others)
|
||||
allowed := map[string]bool{
|
||||
"apt": true,
|
||||
"apt-get": true,
|
||||
"apt-cache": true,
|
||||
"dnf": true,
|
||||
"yum": true,
|
||||
"rpm-ostree": true,
|
||||
"npm": true,
|
||||
"pnpm": true,
|
||||
"bun": true,
|
||||
"pip": true,
|
||||
"pip3": true,
|
||||
"uv": true,
|
||||
"docker": true,
|
||||
"crun": true,
|
||||
"containerd": true,
|
||||
}
|
||||
|
||||
if allowed[packageType] {
|
||||
return true, "allowed_by_policy"
|
||||
}
|
||||
|
||||
return false, "package_manager_not_in_policy"
|
||||
}
|
||||
|
||||
// UpdatePolicy downloads and installs a new WDAC policy from the server
|
||||
func (w *WDACPolicy) UpdatePolicy() error {
|
||||
log.Printf("[INFO] [kernel] [wdac] update_policy_start")
|
||||
|
||||
newHash, err := w.client.GetWdacPolicyHash(w.config.AgentID, w.config.ServerURL)
|
||||
if err != nil {
|
||||
// For now, keep existing policy hash (no update)
|
||||
log.Printf("[WARNING] [kernel] [wdac] policy_update_skipped error=%v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if newHash == "" {
|
||||
return fmt.Errorf("no policy available from server")
|
||||
}
|
||||
|
||||
// TODO: Download policy file and apply via Set-CIPolicy
|
||||
// For now, just update the hash
|
||||
w.policyHash = newHash
|
||||
|
||||
log.Printf("[INFO] [kernel] [wdac] update_policy_complete hash=%s", newHash[:16]+"...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallPolicy installs a WDAC policy from a binary or XML file
|
||||
func (w *WDACPolicy) InstallPolicy(policyPath string) error {
|
||||
// TODO: Implement actual WDAC policy installation via COM API
|
||||
// Uses Microsoft.Management.Ops or similar COM interface
|
||||
|
||||
log.Printf("[INFO] [kernel] [wdac] install_policy path=%s", policyPath)
|
||||
return fmt.Errorf("install_policy_not_implemented_yet")
|
||||
}
|
||||
|
||||
// GetPolicyStatus returns the current WDAC policy status
|
||||
func (w *WDACPolicy) GetPolicyStatus() (string, error) {
|
||||
// TODO: Query WDAC service for status
|
||||
return "unknown", nil
|
||||
}
|
||||
|
||||
// ResetPolicy resets the WDAC policy to a known-good state
|
||||
func (w *WDACPolicy) ResetPolicy() error {
|
||||
log.Printf("[WARNING] [kernel] [wdac] reset_policy_initiated")
|
||||
return fmt.Errorf("reset_policy_not_implemented_yet")
|
||||
}
|
||||
Loading…
Reference in a new issue