Skip to content
39 changes: 39 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "Asia/Dhaka"
open-pull-requests-limit: 10
labels:
- "dependencies"
groups:
gopsutil:
patterns:
- "github.com/shirou/gopsutil/*"
- "github.com/ebitengine/purego"
- "github.com/lufia/plan9stats"
- "github.com/power-devops/perfstat"
- "github.com/tklauser/*"
- "github.com/yusufpapurcu/wmi"
supabase:
patterns:
- "github.com/supabase-community/*"
sqlite:
patterns:
- "modernc.org/*"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "Asia/Dhaka"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "ci"
65 changes: 40 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,10 @@ env:
CGO_ENABLED: '0'

jobs:
lint:
name: Lint (${{ matrix.goos }})
# ── fast, platform-agnostic checks ──────────────────────────────────────────
static-checks:
name: Static Checks
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
goos: [linux, darwin, windows]
steps:
- uses: actions/[email protected]

Expand All @@ -34,21 +28,41 @@ jobs:
cache: true

- name: Verify go.mod is tidy
if: matrix.goos == 'linux'
run: |
go mod tidy
git diff --exit-code go.mod go.sum

- name: Format check
if: matrix.goos == 'linux'
run: |
unformatted=$(gofmt -s -l .)
if [ -n "$unformatted" ]; then
echo "Files not formatted (run 'gofmt -s -w .'):"
echo "$unformatted"
[ -z "$unformatted" ] || { echo "$unformatted"; exit 1; }

- name: Check no CGO
run: |
if grep -rlE '^import "C"' --include="*.go" .; then
echo "CGO import detected — violates CGO_ENABLED=0 policy (see CLAUDE.md)"
exit 1
fi

# ── per-GOOS lint: catches issues in platform-specific files ─────────────────
lint:
name: Lint (${{ matrix.goos }})
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
strategy:
fail-fast: false
matrix:
goos: [linux, darwin, windows]
steps:
- uses: actions/[email protected]

- uses: actions/[email protected]
with:
go-version: ${{ env.GO_VERSION }}
cache: true

- name: Vet
env:
GOOS: ${{ matrix.goos }}
Expand All @@ -61,10 +75,10 @@ jobs:
with:
version: v2.12.2

# ── security: independent — runs in parallel with lint ───────────────────────
security:
name: Security Scan
runs-on: ubuntu-latest
needs: lint
permissions:
contents: read
security-events: write
Expand All @@ -81,6 +95,8 @@ jobs:
- name: Verify modules
run: go mod verify

# Gosec: insight only — uploads findings to GitHub Security tab but does not
# gate the build. govulncheck and Trivy are the enforcing gates below.
- name: Run Gosec
uses: securego/[email protected]
with:
Expand All @@ -97,7 +113,7 @@ jobs:
- name: Run govulncheck
uses: golang/[email protected]

- name: Run Trivy vulnerability scanner
- name: Run Trivy
uses: aquasecurity/[email protected]
with:
scan-type: fs
Expand All @@ -107,10 +123,11 @@ jobs:
exit-code: 1
format: table

# ── tests on real OSes ───────────────────────────────────────────────────────
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: lint
needs: [static-checks, lint]
permissions:
contents: read
strategy:
Expand All @@ -133,14 +150,17 @@ jobs:
shell: bash
run: go tool cover -func=coverage.out

- name: Upload coverage artifact
# Upload all 3 platform coverage files — the codebase has heavy platform-
# specific implementations (every osinfo module has _linux/_darwin/_windows
# files), so all 3 runs are needed for a meaningful merged coverage report.
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.os }}
path: coverage.out
retention-days: 1


# ── cross-compile all targets ─────────────────────────────────────────────────
build:
name: Build (${{ matrix.goos }}/${{ matrix.goarch }})
runs-on: ubuntu-latest
Expand Down Expand Up @@ -182,15 +202,10 @@ jobs:
needs: test
if: github.event_name != 'workflow_call'
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/[email protected]
with:
fetch-depth: 0

- uses: actions/[email protected]
with:
go-version: ${{ env.GO_VERSION }}
cache: true

- name: Download all coverage artifacts
uses: actions/download-artifact@v4
with:
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CodeQL

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
- cron: '0 2 * * 1' # weekly Monday 02:00 UTC — catches new CVEs in unchanged code

permissions:
actions: read
contents: read
security-events: write

env:
CGO_ENABLED: '0'

jobs:
analyze:
name: CodeQL Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]

- uses: actions/[email protected]
with:
go-version: '1.25.11'
cache: true

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
# security-extended adds dataflow/taint-tracking queries on top of the
# default security-and-quality suite — complements Gosec's pattern matching
queries: security-extended

- name: Autobuild
uses: github/codeql-action/autobuild@v3

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: /language:go
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ jobs:
go-version: ${{ env.GO_VERSION }}
cache: true

- name: Ensure shell scripts are executable
run: chmod +x installation-doc/install.sh scripts/*.sh

- name: Build all platforms
run: make all VERSION="${GITHUB_REF#refs/tags/}"

Expand Down
48 changes: 27 additions & 21 deletions cmd/sentinelgo/service/launchd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,29 @@ func createLaunchdPlist() error {
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.sentinelgo.agent</string>
<key>ProgramArguments</key>
<array>
<string>/opt/sentinelgo/sentinelgo</string>
<string>-run</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/sentinelgo.log</string>
<key>StandardErrorPath</key>
<string>/var/log/sentinelgo.err</string>
<key>WorkingDirectory</key>
<string>/opt/sentinelgo</string>
<key>Comment</key>
<string>SentinelGo Agent v%s - Cross-platform system monitoring</string>
<key>Label</key>
<string>com.sentinelgo.agent</string>
<key>ProgramArguments</key>
<array>
<string>/opt/sentinelgo/sentinelgo</string>
<string>-run</string>
<string>--config</string>
<string>/opt/sentinelgo/.sentinelgo/config.json</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/sentinelgo.log</string>
<key>StandardErrorPath</key>
<string>/var/log/sentinelgo.log</string>
<key>UserName</key>
<string>root</string>
<key>WorkingDirectory</key>
<string>/opt/sentinelgo</string>
<key>Comment</key>
<string>SentinelGo Agent v%s - Cross-platform system monitoring</string>
</dict>
</plist>`, version)

Expand All @@ -54,15 +58,17 @@ func createLaunchdPlist() error {
}

func loadLaunchdService() error {
if err := exec.Command("launchctl", "load", "-w", "/Library/LaunchDaemons/com.sentinelgo.agent.plist").Run(); err != nil {
// launchctl bootstrap is the supported API on macOS 10.15+; launchctl load is deprecated.
if err := exec.Command("launchctl", "bootstrap", "system", "/Library/LaunchDaemons/com.sentinelgo.agent.plist").Run(); err != nil {
return fmt.Errorf("load launchd service: %w", err)
}
fmt.Println("Loaded launchd service: com.sentinelgo.agent")
return nil
}

func unloadLaunchdService() error {
if err := exec.Command("launchctl", "unload", "-w", "/Library/LaunchDaemons/com.sentinelgo.agent.plist").Run(); err != nil {
// launchctl bootout is the supported API on macOS 10.15+; launchctl unload is deprecated.
if err := exec.Command("launchctl", "bootout", "system", "/Library/LaunchDaemons/com.sentinelgo.agent.plist").Run(); err != nil {
return fmt.Errorf("unload launchd service: %w", err)
}
fmt.Println("Unloaded launchd service: com.sentinelgo.agent")
Expand Down
9 changes: 9 additions & 0 deletions cmd/sentinelgo/service/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ func HandleInstall(svc AgentService) {
if runtime.GOOS == "darwin" {
fmt.Println("Installing SentinelGo as launchd service...")

// Remove the quarantine flag macOS attaches to downloaded binaries, then
// re-codesign with an ad-hoc identity so Gatekeeper accepts the daemon
// without prompting the user to approve "unidentified developer" software.
// spctl --add registers the binary in the Gatekeeper allowlist.
const binaryPath = "/opt/sentinelgo/sentinelgo"
_ = exec.Command("xattr", "-d", "com.apple.quarantine", binaryPath).Run()
_ = exec.Command("codesign", "--force", "--sign", "-", binaryPath).Run()
_ = exec.Command("spctl", "--add", binaryPath).Run()

if err := createLaunchdPlist(); err != nil {
log.Fatalf("Failed to create launchd plist: %v", err)
}
Expand Down
9 changes: 7 additions & 2 deletions installation-doc/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,19 @@ setup_directories() {

# Set permissions
chmod +x "$INSTALL_DIR/$BINARY_NAME"

local os=$(detect_os)

# Set permissions based on OS
if [[ "$os" == "macos" ]]; then
# macOS: Use chown with proper group handling
chown -R "$SERVICE_USER" "$INSTALL_DIR" 2>/dev/null || true
chmod -R 755 "$INSTALL_DIR" 2>/dev/null || true
# Remove download quarantine flag and register with Gatekeeper so macOS does
# not block the daemon with "cannot be verified for malware" on first run.
xattr -d com.apple.quarantine "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || true
codesign --force --sign - "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || true
spctl --add "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || true
elif [[ "$os" == "windows" ]]; then
# Windows: Skip ownership change
echo "[INFO] Skipping ownership change on Windows"
Expand Down
Loading
Loading