Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f624e9b
Implement enhanced proxy management and logging in the daemon
tudor-timcu Feb 5, 2026
c9acad1
Update macOS plist to enable KeepAlive for the daemon
tudor-timcu Feb 5, 2026
d13a4f8
Enhance scanner functionality and logging
tudor-timcu Feb 6, 2026
840203e
Update SafechainScanner to disable logging during version command exe…
tudor-timcu Feb 6, 2026
4d35b38
Refactor SafechainScanner name and improve logging control in RunComm…
tudor-timcu Feb 6, 2026
f8000dd
Update SafeChainUltimate.wxs to include OnInstall attribute for servi…
tudor-timcu Feb 6, 2026
da9498c
Enhance SafeChainUltimate service configuration in Windows installer
tudor-timcu Feb 6, 2026
1d22f7d
Add WixToolset.Util extension to Windows MSI build script
tudor-timcu Feb 6, 2026
19aef1f
Add WiX extensions to Windows MSI build script
tudor-timcu Feb 6, 2026
e20f545
Remove file ownership setting in DownloadBinary function
tudor-timcu Feb 6, 2026
d71929b
Update SafeChainUltimate service restart delay in Windows installer
tudor-timcu Feb 6, 2026
da94221
Enhance daemon logging and add proxy version retrieval
tudor-timcu Feb 6, 2026
48bfbbc
Remove unnecessary logging in runAsLoggedInUser function in platform_…
tudor-timcu Feb 6, 2026
d0a6b2c
Merge branch 'main' into feat/retry-limits
tudor-timcu Feb 6, 2026
7f3190a
Update service restart delay in macOS and Windows configurations
tudor-timcu Feb 9, 2026
5290060
Apply suggestion from @bitterpanda63
bitterpanda63 Feb 9, 2026
9a901ef
Refactor daemon logging and enhance proxy version handling
tudor-timcu Feb 9, 2026
3dc1df5
Enhance proxy error handling in daemon
tudor-timcu Feb 9, 2026
a329518
Refactor handleProxy method to return retry status
tudor-timcu Feb 9, 2026
1cbbdda
Improve logging in handleProxy method to include retry interval
tudor-timcu Feb 9, 2026
1f99fb0
Update WiX extensions in Windows build workflow to specific version 6…
tudor-timcu Feb 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,10 @@ jobs:
- name: Install WiX Toolset
run: dotnet tool install -g wix

- name: Add WiX UI extension
run: wix extension add WixToolset.UI.wixext
- name: Add WiX extensions
run: |
wix extension add WixToolset.UI.wixext/6.0.2
wix extension add WixToolset.Util.wixext/6.0.2

- name: Build MSI installer
run: |
Expand Down
84 changes: 74 additions & 10 deletions internal/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import (
"github.com/AikidoSec/safechain-internals/internal/version"
)

const (
DaemonStatusLogInterval = 1 * time.Hour
ProxyStartMaxRetries = 20
ProxyStartRetryInterval = 3 * time.Minute
)

type Config struct {
ConfigPath string
LogLevel string
Expand All @@ -33,6 +39,11 @@ type Daemon struct {
ingress *ingress.Server
logRotator *utils.LogRotator
logReaper *utils.LogReaper

proxyRetryCount int
proxyLastRetryTime time.Time

daemonLastStatusLogTime time.Time
}

func New(ctx context.Context, cancel context.CancelFunc, config *Config) (*Daemon, error) {
Expand Down Expand Up @@ -213,19 +224,72 @@ func (d *Daemon) Uninstall(ctx context.Context, removeScanners bool) error {
return nil
}

func (d *Daemon) heartbeat() error {
if !d.proxy.IsProxyRunning() {
log.Println("Proxy is not running, starting it...")
if err := d.startProxyAndInstallCA(d.ctx); err != nil {
return fmt.Errorf("failed to start proxy and install CA: %v", err)
func (d *Daemon) printDaemonStatus() {
log.Println("Daemon status:")
if d.proxy.IsProxyRunning() {
proxyVersion, _ := d.proxy.Version()
log.Printf("\t- Proxy: %s", proxyVersion)
} else {
log.Println("\t- Proxy: not running!")
}

for _, scannerName := range d.registry.List() {
scanner, err := d.registry.Get(scannerName)
if err != nil {
continue
}
if d.proxy.IsProxyRunning() {
log.Println("Proxy started successfully")
} else {
log.Println("Failed to start proxy, will try again later")
if scanner.IsInstalled(d.ctx) {
log.Printf("\t- %s: %s", scannerName, scanner.Version(d.ctx))
}
}
}

func (d *Daemon) handleProxy() (shouldRetry bool, err error) {
if d.proxy.IsProxyRunning() {
return true, nil
}

if d.proxyRetryCount >= ProxyStartMaxRetries {
// Exit daemon loop if proxy start retry limit is reached
return false, fmt.Errorf("proxy start retry limit reached (%d attempts), not retrying", d.proxyRetryCount)
}

if !d.proxyLastRetryTime.IsZero() && time.Since(d.proxyLastRetryTime) < ProxyStartRetryInterval {
log.Printf("Proxy is not running, waiting for retry interval (%s) before next attempt", ProxyStartRetryInterval)
return true, nil
}

d.proxyRetryCount++
d.proxyLastRetryTime = time.Now()
log.Printf("Proxy is not running, starting it... (attempt %d/%d)", d.proxyRetryCount, ProxyStartMaxRetries)

if err := d.startProxyAndInstallCA(d.ctx); err != nil {
log.Printf("Failed to start proxy and install CA: %v", err)
return true, nil
}

if d.proxy.IsProxyRunning() {
log.Println("Proxy started successfully")
d.proxyRetryCount = 0
d.proxyLastRetryTime = time.Time{}
} else {
log.Println("Proxy is running")
log.Printf("Failed to start proxy, will try again later")
}
return true, nil
}

func (d *Daemon) heartbeat() error {
shouldRetry, err := d.handleProxy()
if !shouldRetry {
return fmt.Errorf("failed to handle proxy: %v", err)
}
if err != nil {
log.Printf("Failed to start proxy: %v", err)
}

if time.Since(d.daemonLastStatusLogTime) >= DaemonStatusLogInterval {
d.printDaemonStatus()
d.daemonLastStatusLogTime = time.Now()
}
return nil
}
Expand Down
3 changes: 0 additions & 3 deletions internal/platform/platform_windows_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,6 @@ func runAsLoggedInUser(binaryPath string, args []string) (string, error) {
return "", err
}

log.Printf("Found active user session: %d", sessionID)

var userToken windows.Token
if err := windows.WTSQueryUserToken(sessionID, &userToken); err != nil {
return "", fmt.Errorf("WTSQueryUserToken failed: %v", err)
Expand Down Expand Up @@ -192,7 +190,6 @@ func runAsLoggedInUser(binaryPath string, args []string) (string, error) {
return "", fmt.Errorf("runProcessAsUser failed: %v", err)
}

log.Printf("Process %s completed successfully as logged-in user", binaryPath)
return output, nil
}

Expand Down
18 changes: 18 additions & 0 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/AikidoSec/safechain-internals/internal/platform"
Expand Down Expand Up @@ -55,6 +56,23 @@ func (p *Proxy) WaitForProxyToBeReady() error {
}
}

func (p *Proxy) Version() (string, error) {
cmd := exec.Command(filepath.Join(platform.GetConfig().BinaryDir, platform.SafeChainProxyBinaryName), "--version")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get proxy version: %v", err)
}
trimmed := strings.TrimSpace(string(output))
if trimmed == "" {
return "", fmt.Errorf("proxy version output is empty")
}
parts := strings.Split(trimmed, " ")
if len(parts) == 0 {
return "", fmt.Errorf("failed to parse proxy version from output: %q", trimmed)
}
return parts[len(parts)-1], nil
}

func (p *Proxy) Start(ctx context.Context, proxyIngressAddr string) error {
config := platform.GetConfig()
p.ctx, p.cancel = context.WithCancel(ctx)
Expand Down
4 changes: 4 additions & 0 deletions internal/scanner/githook/githook.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ func (s *GitHookScanner) Name() string {
return "githook"
}

func (s *GitHookScanner) Version(ctx context.Context) string {
return ""
}

func (s *GitHookScanner) Install(ctx context.Context) error {
select {
case <-ctx.Done():
Expand Down
4 changes: 2 additions & 2 deletions internal/scanner/safechain/safechain.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ func New() scanner.Scanner {
}

func (s *SafechainScanner) Name() string {
return "safechain"
return "Safechain"
}

func (s *SafechainScanner) Version(ctx context.Context) string {
output, err := platform.RunAsCurrentUser(ctx, platform.GetConfig().SafeChainBinaryPath, []string{"-v"})
output, err := platform.RunAsCurrentUser(context.WithValue(ctx, "disable_logging", true), platform.GetConfig().SafeChainBinaryPath, []string{"-v"})
if err != nil {
return ""
}
Expand Down
1 change: 1 addition & 0 deletions internal/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

type Scanner interface {
Name() string
Version(ctx context.Context) string
Install(ctx context.Context) error
Uninstall(ctx context.Context) error
IsInstalled(ctx context.Context) bool
Expand Down
4 changes: 4 additions & 0 deletions internal/scanner/vscode/vscode.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ func (s *VSCodeScanner) Name() string {
return "vscode"
}

func (s *VSCodeScanner) Version(ctx context.Context) string {
return ""
}

func (s *VSCodeScanner) Install(ctx context.Context) error {
select {
case <-ctx.Done():
Expand Down
11 changes: 8 additions & 3 deletions internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ func DetectArch() string {
}
}


func DownloadBinary(ctx context.Context, url, destPath string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
Expand Down Expand Up @@ -208,11 +207,17 @@ func RunCommands(ctx context.Context, commands []Command) ([]string, error) {
}

func RunCommandWithEnv(ctx context.Context, env []string, command string, args ...string) (string, error) {
log.Printf("Running command: %s %s", command, strings.Join(args, " "))
disableLogging, ok := ctx.Value("disable_logging").(bool)
if !ok {
disableLogging = false
}
if !disableLogging {
log.Printf("Running command: %s %s", command, strings.Join(args, " "))
}
cmd := exec.CommandContext(ctx, command, args...)
cmd.Env = append(os.Environ(), env...)
output, err := cmd.CombinedOutput()
if err != nil {
if err != nil && !disableLogging {
log.Printf("\t- Command error: %v", err)
log.Printf("\t- Command output: %s", string(output))
}
Expand Down
2 changes: 2 additions & 0 deletions packaging/macos/com.aikidosecurity.safechainultimate.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<dict>
<key>KeepAlive</key>
<true/>
<key>ThrottleInterval</key>
<integer>60</integer>
<key>Label</key>
<string>com.aikidosecurity.safechainultimate</string>
<key>LimitLoadToSessionType</key>
Expand Down
10 changes: 9 additions & 1 deletion packaging/windows/SafeChainUltimate.wxs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
<Package Name="SafeChain Ultimate"
Manufacturer="Aikido Security BV"
Version="$(var.Version)"
Expand Down Expand Up @@ -54,6 +55,13 @@
Type="ownProcess"
ErrorControl="normal"
Account="LocalSystem" />
<util:ServiceConfig
ServiceName="SafeChainUltimate"
FirstFailureActionType="restart"
SecondFailureActionType="restart"
ThirdFailureActionType="restart"
RestartServiceDelayInSeconds="60"
ResetPeriodInDays="1" />
<ServiceControl Id="SafeChainUltimateServiceControl"
Name="SafeChainUltimate"
Start="install"
Expand Down
1 change: 1 addition & 0 deletions packaging/windows/build-msi.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ wix build $WxsFile `
-d BinDir=$BinDir `
-d ProjectDir=$ProjectDir `
-ext WixToolset.UI.wixext `
-ext WixToolset.Util.wixext `
-arch $WixArch `
-o $OutputMsi

Expand Down