diff --git a/.devcontainer/alpine/Dockerfile b/.devcontainer/alpine/Dockerfile new file mode 100644 index 00000000..f1859bc7 --- /dev/null +++ b/.devcontainer/alpine/Dockerfile @@ -0,0 +1,27 @@ +FROM golang:1.25-alpine AS go-builder + +FROM alpine:3.21 + +COPY --from=go-builder /usr/local/go /usr/local/go +ENV PATH="/usr/local/go/bin:/root/go/bin:/root/.cargo/bin:${PATH}" +ENV GOPATH="/root/go" + +RUN apk add --no-cache \ + build-base git make curl sudo pkgconf \ + rpm \ + ruby ruby-dev \ + nodejs npm py3-pip \ + wayland-dev libx11-dev libxkbcommon-dev mesa-dev libxcursor-dev vulkan-loader-dev libffi-dev \ + perl dpkg dpkg-dev xz wget + +RUN gem install fpm --no-document + +RUN cd /tmp && \ + wget -q "https://deb.debian.org/debian/pool/main/a/alien/alien_8.95.8.tar.xz" && \ + tar xf alien_8.95.8.tar.xz && \ + cd alien && perl Makefile.PL && make && make install && \ + cd / && rm -rf /tmp/alien* + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +WORKDIR /workspace diff --git a/.devcontainer/alpine/devcontainer.json b/.devcontainer/alpine/devcontainer.json new file mode 100644 index 00000000..0819a62e --- /dev/null +++ b/.devcontainer/alpine/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "SafeChain Ultimate - Alpine", + "build": { + "dockerfile": "Dockerfile" + }, + "remoteUser": "root" +} diff --git a/.devcontainer/centos/Dockerfile b/.devcontainer/centos/Dockerfile new file mode 100644 index 00000000..5348b428 --- /dev/null +++ b/.devcontainer/centos/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:1.25 AS go-builder + +FROM quay.io/centos/centos:stream10 + +COPY --from=go-builder /usr/local/go /usr/local/go +ENV PATH="/usr/local/go/bin:/root/go/bin:/root/.cargo/bin:${PATH}" +ENV GOPATH="/root/go" + +RUN dnf install -y epel-release && \ + dnf config-manager --set-enabled crb && \ + dnf install -y \ + gcc gcc-c++ git make cmake curl sudo pkgconf clang-devel procps-ng \ + rpm-build \ + dpkg \ + ruby ruby-devel \ + nodejs npm python3-pip \ + perl wget xz \ + wayland-devel libX11-devel libxkbcommon-devel libxkbcommon-x11-devel \ + mesa-libGLES-devel mesa-libEGL-devel libffi-devel libXcursor-devel vulkan-loader-devel \ + && gem install fpm --no-document \ + && dnf clean all + +RUN cd /tmp && \ + wget -q "https://deb.debian.org/debian/pool/main/a/alien/alien_8.95.8.tar.xz" && \ + tar xf alien_8.95.8.tar.xz && \ + cd alien && perl Makefile.PL && make && make install && \ + cd / && rm -rf /tmp/alien* + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +WORKDIR /workspace diff --git a/.devcontainer/centos/devcontainer.json b/.devcontainer/centos/devcontainer.json new file mode 100644 index 00000000..2a1c9813 --- /dev/null +++ b/.devcontainer/centos/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "SafeChain Ultimate - CentOS Stream 10", + "build": { + "dockerfile": "Dockerfile" + }, + "remoteUser": "root" +} diff --git a/.devcontainer/debian/Dockerfile b/.devcontainer/debian/Dockerfile new file mode 100644 index 00000000..b6434dec --- /dev/null +++ b/.devcontainer/debian/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.25 AS go-builder + +FROM debian:12 + +COPY --from=go-builder /usr/local/go /usr/local/go +ENV PATH="/usr/local/go/bin:/root/go/bin:/root/.cargo/bin:${PATH}" +ENV GOPATH="/root/go" + +RUN apt-get update && apt-get install -y \ + build-essential git make curl sudo pkg-config \ + rpm alien \ + ruby ruby-dev \ + nodejs npm python3-pip \ + libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev \ + libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev \ + && gem install fpm --no-document \ + && rm -rf /var/lib/apt/lists/* + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +WORKDIR /workspace diff --git a/.devcontainer/debian/devcontainer.json b/.devcontainer/debian/devcontainer.json new file mode 100644 index 00000000..00711d15 --- /dev/null +++ b/.devcontainer/debian/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "SafeChain Ultimate - Debian", + "build": { + "dockerfile": "Dockerfile" + }, + "remoteUser": "root" +} diff --git a/.devcontainer/ubuntu/Dockerfile b/.devcontainer/ubuntu/Dockerfile new file mode 100644 index 00000000..8d454620 --- /dev/null +++ b/.devcontainer/ubuntu/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.25 AS go-builder + +FROM ubuntu:24.04 + +COPY --from=go-builder /usr/local/go /usr/local/go +ENV PATH="/usr/local/go/bin:/root/go/bin:/root/.cargo/bin:${PATH}" +ENV GOPATH="/root/go" + +RUN apt-get update && apt-get install -y \ + build-essential git make curl sudo pkg-config \ + rpm alien \ + ruby ruby-dev \ + nodejs npm python3-pip \ + libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev \ + libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev \ + && gem install fpm --no-document \ + && rm -rf /var/lib/apt/lists/* + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + +WORKDIR /workspace diff --git a/.devcontainer/ubuntu/devcontainer.json b/.devcontainer/ubuntu/devcontainer.json new file mode 100644 index 00000000..ec646f8f --- /dev/null +++ b/.devcontainer/ubuntu/devcontainer.json @@ -0,0 +1,7 @@ +{ + "name": "SafeChain Ultimate - Ubuntu", + "build": { + "dockerfile": "Dockerfile" + }, + "remoteUser": "root" +} diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 00000000..3ae285d1 --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,103 @@ +name: Build Linux Packages + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_call: + inputs: + version: + required: true + type: string + +jobs: + build-unix: + uses: ./.github/workflows/build-unix.yml + with: + version: ${{ inputs.version || 'dev' }} + + build-rpm: + needs: build-unix + strategy: + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm + + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download agent artifact + uses: actions/download-artifact@v4 + with: + name: safechain-ultimate-linux-${{ matrix.arch }} + path: bin + + - name: Download agent ui artifact + uses: actions/download-artifact@v4 + with: + name: safechain-ultimate-ui-linux-${{ matrix.arch }} + path: bin + + - name: Download proxy artifact + uses: actions/download-artifact@v4 + with: + name: safechain-proxy-linux-${{ matrix.arch }} + path: bin + + - name: Verify binaries exist + run: | + ls -lh bin/ + ls -lh bin/safechain-ultimate-linux-${{ matrix.arch }} + ls -lh bin/safechain-ultimate-ui-linux-${{ matrix.arch }} + ls -lh bin/safechain-proxy-linux-${{ matrix.arch }} + + - name: Install packaging tools + run: | + sudo apt-get update && sudo apt-get install -y rpm alien ruby ruby-dev + sudo gem install fpm --no-document + + - name: Build RPM + run: | + cd packaging/rpm + ./build-rpm.sh -v "${{ inputs.version || 'dev' }}" -a "${{ matrix.arch }}" -b "../../bin" -o "../../dist" + + - name: Convert RPM to DEB using alien + run: | + cd dist + sudo alien --to-deb --keep-version SafeChainUltimate-${{ inputs.version || 'dev' }}-${{ matrix.arch }}.rpm + + - name: Convert RPM to APK using fpm + run: | + cd dist + fpm -s rpm -t apk -p SafeChainUltimate-${{ matrix.arch }}.apk SafeChainUltimate-${{ inputs.version || 'dev' }}-${{ matrix.arch }}.rpm + + - name: Rename packages + run: | + mv dist/SafeChainUltimate-${{ inputs.version || 'dev' }}-${{ matrix.arch }}.rpm dist/SafeChainUltimate-${{ matrix.arch }}.rpm + mv dist/safechain-ultimate_*.deb dist/SafeChainUltimate-${{ matrix.arch }}.deb + + - name: Upload RPM artifact + uses: actions/upload-artifact@v4 + with: + name: SafeChainUltimate-${{ matrix.arch }}.rpm + path: dist/SafeChainUltimate-${{ matrix.arch }}.rpm + + - name: Upload DEB artifact + uses: actions/upload-artifact@v4 + with: + name: SafeChainUltimate-${{ matrix.arch }}.deb + path: dist/SafeChainUltimate-${{ matrix.arch }}.deb + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: SafeChainUltimate-${{ matrix.arch }}.apk + path: dist/SafeChainUltimate-${{ matrix.arch }}.apk diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 413ad334..e1e4cab4 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -18,10 +18,18 @@ jobs: strategy: matrix: include: - - arch: amd64 + - os: darwin + arch: amd64 runner: macos-14 - - arch: arm64 + - os: darwin + arch: arm64 runner: macos-14 + - os: linux + arch: amd64 + runner: ubuntu-latest + - os: linux + arch: arm64 + runner: ubuntu-24.04-arm runs-on: ${{ matrix.runner }} steps: @@ -33,29 +41,35 @@ jobs: with: go-version: "1.25" + - name: Install Linux GUI dependencies + if: matrix.os == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev + - name: Run tests run: make test - - name: Build binaries for darwin/${{ matrix.arch }} - run: make build-darwin-${{ matrix.arch }} VERSION="${{ inputs.version || 'dev' }}" + - name: Build binaries for ${{ matrix.os }}/${{ matrix.arch }} + run: make build-${{ matrix.os }}-${{ matrix.arch }} VERSION="${{ inputs.version || 'dev' }}" - name: Prepare artifacts run: | - mv bin/safechain-ultimate-darwin-${{ matrix.arch }} safechain-ultimate-darwin-${{ matrix.arch }} - mv bin/safechain-ultimate-ui-darwin-${{ matrix.arch }} safechain-ultimate-ui-darwin-${{ matrix.arch }} + mv bin/safechain-ultimate-${{ matrix.os }}-${{ matrix.arch }} safechain-ultimate-${{ matrix.os }}-${{ matrix.arch }} + mv bin/safechain-ultimate-ui-${{ matrix.os }}-${{ matrix.arch }} safechain-ultimate-ui-${{ matrix.os }}-${{ matrix.arch }} - name: Upload safechain-ultimate artifact uses: actions/upload-artifact@v4 with: - name: safechain-ultimate-darwin-${{ matrix.arch }} + name: safechain-ultimate-${{ matrix.os }}-${{ matrix.arch }} path: | - safechain-ultimate-darwin-${{ matrix.arch }} + safechain-ultimate-${{ matrix.os }}-${{ matrix.arch }} - name: Upload safechain-ultimate-ui artifact uses: actions/upload-artifact@v4 with: - name: safechain-ultimate-ui-darwin-${{ matrix.arch }} + name: safechain-ultimate-ui-${{ matrix.os }}-${{ matrix.arch }} path: | - safechain-ultimate-ui-darwin-${{ matrix.arch }} + safechain-ultimate-ui-${{ matrix.os }}-${{ matrix.arch }} build-unix-proxy: strategy: diff --git a/Makefile b/Makefile index 4c4334ac..4f248e05 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-release build-darwin-amd64 build-darwin-arm64 build-windows-amd64 build-windows-arm64 build-proxy build-pkg build-pkg-sign-local install-pkg uninstall-pkg clean test run help +.PHONY: build build-release build-darwin-amd64 build-darwin-arm64 build-linux-amd64 build-linux-arm64 build-windows-amd64 build-windows-arm64 build-proxy build-pkg build-pkg-sign-local install-pkg uninstall-pkg build-rpm build-deb build-apk install-rpm install-deb install-apk uninstall-rpm uninstall-deb uninstall-apk clean test run help BINARY_NAME=safechain-ultimate BINARY_NAME_UI=safechain-ultimate-ui @@ -82,6 +82,12 @@ build-darwin-amd64: build-darwin-arm64: @$(MAKE) GOOS=darwin GOARCH=arm64 build-release +build-linux-amd64: + @$(MAKE) GOOS=linux GOARCH=amd64 build-release + +build-linux-arm64: + @$(MAKE) GOOS=linux GOARCH=arm64 build-release + build-windows-amd64: @$(MAKE) GOOS=windows GOARCH=amd64 build-release @@ -105,6 +111,96 @@ else @exit 1 endif +build-rpm: build-release build-proxy +ifeq ($(DETECTED_OS),linux) + @echo "Building Linux RPM installer..." + @cd packaging/rpm && ./build-rpm.sh -v $(VERSION) -a $(DETECTED_ARCH) -b ../../$(BIN_DIR) -o ../../$(DIST_DIR) + @echo "RPM built in $(DIST_DIR)/" +else + @echo "Error: RPM building is only supported on Linux" + @exit 1 +endif + +build-deb: build-rpm +ifeq ($(DETECTED_OS),linux) + @echo "Converting RPM to DEB using alien..." + @cd $(DIST_DIR) && sudo alien --to-deb --keep-version SafeChainUltimate-$(VERSION)-$(DETECTED_ARCH).rpm + @echo "DEB built in $(DIST_DIR)/" +else + @echo "Error: DEB building is only supported on Linux" + @exit 1 +endif + +build-apk: build-rpm +ifeq ($(DETECTED_OS),linux) + @echo "Converting RPM to Alpine APK using fpm..." + @cd $(DIST_DIR) && fpm -s rpm -t apk SafeChainUltimate-$(VERSION)-$(DETECTED_ARCH).rpm + @echo "APK built in $(DIST_DIR)/" +else + @echo "Error: APK building is only supported on Linux" + @exit 1 +endif + +install-rpm: build-rpm +ifeq ($(DETECTED_OS),linux) + @echo "Installing RPM package..." + @sudo rpm -U --force $(DIST_DIR)/SafeChainUltimate-$(VERSION)-$(DETECTED_ARCH).rpm + @echo "RPM package installed." +else + @echo "Error: RPM installation is only supported on Linux" + @exit 1 +endif + +uninstall-rpm: +ifeq ($(DETECTED_OS),linux) + @echo "Uninstalling RPM package..." + @sudo rpm -e safechain-ultimate + @echo "RPM package uninstalled." +else + @echo "Error: RPM uninstallation is only supported on Linux" + @exit 1 +endif + +install-deb: build-deb +ifeq ($(DETECTED_OS),linux) + @echo "Installing DEB package..." + @sudo dpkg -i $(DIST_DIR)/safechain-ultimate_$(VERSION)*.deb + @echo "DEB package installed." +else + @echo "Error: DEB installation is only supported on Linux" + @exit 1 +endif + +uninstall-deb: +ifeq ($(DETECTED_OS),linux) + @echo "Uninstalling DEB package..." + @sudo dpkg -r safechain-ultimate + @echo "DEB package uninstalled." +else + @echo "Error: DEB uninstallation is only supported on Linux" + @exit 1 +endif + +install-apk: build-apk +ifeq ($(DETECTED_OS),linux) + @echo "Installing APK package..." + @sudo apk add --allow-untrusted $(DIST_DIR)/SafeChainUltimate-$(VERSION)*.apk + @echo "APK package installed." +else + @echo "Error: APK installation is only supported on Linux" + @exit 1 +endif + +uninstall-apk: +ifeq ($(DETECTED_OS),linux) + @echo "Uninstalling APK package..." + @sudo apk del safechain-ultimate + @echo "APK package uninstalled." +else + @echo "Error: APK uninstallation is only supported on Linux" + @exit 1 +endif + build-pkg-sign-local: ifeq ($(DETECTED_OS),darwin) @echo "Building complete macOS package..." diff --git a/internal/platform/platform.go b/internal/platform/platform.go index e630be91..2a09f601 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -1,6 +1,7 @@ package platform import ( + "context" "fmt" "os" "path/filepath" @@ -57,3 +58,8 @@ func GetProxyLogPath() string { func GetProxyErrLogPath() string { return filepath.Join(config.LogDir, SafeChainProxyErrLogName) } + +type ServiceRunner interface { + Start(ctx context.Context) error + Stop(ctx context.Context) error +} diff --git a/internal/platform/platform_darwin.go b/internal/platform/platform_darwin.go index 00d53860..98350476 100644 --- a/internal/platform/platform_darwin.go +++ b/internal/platform/platform_darwin.go @@ -19,12 +19,12 @@ import ( ) const ( - SafeChainUltimateLogName = "safechain-ultimate.log" - SafeChainUltimateErrLogName = "safechain-ultimate.error.log" - SafeChainUIBinaryName = "safechain-ultimate-ui" - SafeChainProxyBinaryName = "safechain-proxy" - SafeChainProxyLogName = "safechain-proxy.log" - SafeChainProxyErrLogName = "safechain-proxy.err" + SafeChainUltimateLogName = "safechain-ultimate.log" + SafeChainUltimateErrLogName = "safechain-ultimate.error.log" + SafeChainUIBinaryName = "safechain-ultimate-ui" + SafeChainProxyBinaryName = "safechain-proxy" + SafeChainProxyLogName = "safechain-proxy.log" + SafeChainProxyErrLogName = "safechain-proxy.err" SafeChainInstallScriptName = "install-safe-chain.sh" SafeChainUninstallScriptName = "uninstall-safe-chain.sh" ) @@ -302,11 +302,6 @@ func UninstallProxyCA(ctx context.Context) error { return nil } -type ServiceRunner interface { - Start(ctx context.Context) error - Stop(ctx context.Context) error -} - func IsWindowsService() bool { return false } diff --git a/internal/platform/platform_linux.go b/internal/platform/platform_linux.go new file mode 100644 index 00000000..9ec9b727 --- /dev/null +++ b/internal/platform/platform_linux.go @@ -0,0 +1,388 @@ +//go:build linux + +package platform + +import ( + "context" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/AikidoSec/safechain-internals/internal/utils" +) + +const ( + SafeChainUltimateLogName = "safechain-ultimate.log" + SafeChainUltimateErrLogName = "safechain-ultimate.error.log" + SafeChainUIBinaryName = "safechain-ultimate-ui" + SafeChainProxyBinaryName = "safechain-proxy" + SafeChainProxyLogName = "safechain-proxy.log" + SafeChainProxyErrLogName = "safechain-proxy.err" + SafeChainInstallScriptName = "install-safe-chain.sh" + SafeChainUninstallScriptName = "uninstall-safe-chain.sh" + + debianCertDir = "/usr/local/share/ca-certificates" + rhelCertDir = "/etc/pki/ca-trust/source/anchors" + certFileName = "aikidosafechain.crt" +) + +func getCertDir() string { + if _, err := exec.LookPath("update-ca-trust"); err == nil { + return rhelCertDir + } + return debianCertDir +} + +func updateCACertificates() error { + if path, err := exec.LookPath("update-ca-trust"); err == nil { + _, err := exec.Command(path, "extract").Output() + return err + } + if path, err := exec.LookPath("update-ca-certificates"); err == nil { + _, err := exec.Command(path).Output() + return err + } + return fmt.Errorf("no supported CA certificate update tool found (tried update-ca-trust, update-ca-certificates)") +} + +func refreshCACertificates() error { + if path, err := exec.LookPath("update-ca-trust"); err == nil { + _, err := exec.Command(path, "extract").Output() + return err + } + if path, err := exec.LookPath("update-ca-certificates"); err == nil { + _, err := exec.Command(path, "--fresh").Output() + return err + } + return fmt.Errorf("no supported CA certificate update tool found (tried update-ca-trust, update-ca-certificates)") +} + +func initConfig() error { + if RunningAsRoot() { + username, err := getLoggedInUser(context.Background()) + if err != nil { + log.Printf("Warning: %v, falling back to root home directory", err) + config.HomeDir = "/root" + } else { + config.HomeDir = filepath.Join("/home", username) + } + } else { + var err error + config.HomeDir, err = os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + } + safeChainHomeDir := filepath.Join(config.HomeDir, ".safe-chain") + config.BinaryDir = "/opt/aikidosecurity/safechainultimate/bin" + config.RunDir = "/opt/aikidosecurity/safechainultimate/run" + config.LogDir = "/var/log/aikidosecurity/safechainultimate" + config.SafeChainBinaryPath = filepath.Join(safeChainHomeDir, "bin", "safe-chain") + return nil +} + +func PrepareShellEnvironment(_ context.Context) error { + return nil +} + +func SetupLogging() (io.Writer, error) { + return os.Stdout, nil +} + +func getGsettingsProxy(ctx context.Context) (string, error) { + output, err := exec.CommandContext(ctx, "gsettings", "get", "org.gnome.system.proxy", "mode").Output() + if err != nil { + return "", err + } + return strings.TrimSpace(strings.Trim(string(output), "'")), nil +} + +func getGsettingsAutoConfigURL(ctx context.Context) (string, error) { + output, err := exec.CommandContext(ctx, "gsettings", "get", "org.gnome.system.proxy", "autoconfig-url").Output() + if err != nil { + return "", err + } + return strings.TrimSpace(strings.Trim(string(output), "'")), nil +} + +func hasGsettings() bool { + if _, err := exec.LookPath("gsettings"); err != nil { + return false + } + err := exec.Command("gsettings", "get", "org.gnome.system.proxy", "mode").Run() + return err == nil +} + +func SetSystemPAC(ctx context.Context, pacURL string) error { + if !hasGsettings() { + log.Println("gsettings not available, setting proxy environment variables only") + return setEnvironmentProxy(pacURL) + } + + log.Printf("Setting system PAC to %q via gsettings\n", pacURL) + if err := exec.CommandContext(ctx, "gsettings", "set", "org.gnome.system.proxy", "autoconfig-url", pacURL).Run(); err != nil { + return fmt.Errorf("failed to set autoconfig-url: %v", err) + } + if err := exec.CommandContext(ctx, "gsettings", "set", "org.gnome.system.proxy", "mode", "auto").Run(); err != nil { + return fmt.Errorf("failed to set proxy mode to auto: %v", err) + } + return nil +} + +func IsSystemPACSet(ctx context.Context, pacURL string) error { + if !hasGsettings() { + return isEnvironmentProxySet(pacURL) + } + + mode, err := getGsettingsProxy(ctx) + if err != nil { + return fmt.Errorf("failed to get proxy mode: %v", err) + } + if mode != "auto" { + return fmt.Errorf("proxy mode is %q, expected 'auto'", mode) + } + + configURL, err := getGsettingsAutoConfigURL(ctx) + if err != nil { + return fmt.Errorf("failed to get autoconfig-url: %v", err) + } + if configURL != pacURL { + return fmt.Errorf("autoconfig-url is %q, expected %q", configURL, pacURL) + } + return nil +} + +func IsAnySystemProxySet(ctx context.Context) (bool, error) { + if !hasGsettings() { + return isAnyEnvironmentProxySet(), nil + } + + mode, err := getGsettingsProxy(ctx) + if err != nil { + return false, fmt.Errorf("failed to get proxy mode: %v", err) + } + return mode != "none" && mode != "", nil +} + +func UnsetSystemPAC(ctx context.Context, pacURL string) error { + if !hasGsettings() { + return unsetEnvironmentProxy() + } + + log.Println("Unsetting system PAC via gsettings") + errs := []error{} + if err := exec.CommandContext(ctx, "gsettings", "set", "org.gnome.system.proxy", "autoconfig-url", "").Run(); err != nil { + errs = append(errs, err) + } + if err := exec.CommandContext(ctx, "gsettings", "set", "org.gnome.system.proxy", "mode", "none").Run(); err != nil { + errs = append(errs, err) + } + if len(errs) > 0 { + return fmt.Errorf("failed to unset system PAC: %v", errs) + } + return nil +} + +func setEnvironmentProxy(pacURL string) error { + proxyEnvFile := "/etc/profile.d/safechain-proxy.sh" + content := fmt.Sprintf("# SafeChain Ultimate proxy configuration\nexport auto_proxy=\"%s\"\n", pacURL) + if err := os.WriteFile(proxyEnvFile, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write proxy environment file: %v", err) + } + return nil +} + +func isEnvironmentProxySet(pacURL string) error { + proxyEnvFile := "/etc/profile.d/safechain-proxy.sh" + content, err := os.ReadFile(proxyEnvFile) + if err != nil { + return fmt.Errorf("proxy environment file not found: %v", err) + } + if !strings.Contains(string(content), pacURL) { + return fmt.Errorf("proxy environment file does not contain expected PAC URL") + } + return nil +} + +func isAnyEnvironmentProxySet() bool { + proxyEnvFile := "/etc/profile.d/safechain-proxy.sh" + _, err := os.Stat(proxyEnvFile) + return err == nil +} + +func unsetEnvironmentProxy() error { + proxyEnvFile := "/etc/profile.d/safechain-proxy.sh" + if err := os.Remove(proxyEnvFile); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove proxy environment file: %v", err) + } + return nil +} + +func InstallProxyCA(_ context.Context, certPath string) error { + certDir := getCertDir() + destPath := filepath.Join(certDir, certFileName) + if err := os.MkdirAll(certDir, 0755); err != nil { + return fmt.Errorf("failed to create certificate directory: %v", err) + } + input, err := os.ReadFile(certPath) + if err != nil { + return fmt.Errorf("failed to read certificate: %v", err) + } + if err := os.WriteFile(destPath, input, 0644); err != nil { + return fmt.Errorf("failed to write certificate: %v", err) + } + if err := updateCACertificates(); err != nil { + return fmt.Errorf("failed to update ca certificates: %v", err) + } + return nil +} + +func IsProxyCAInstalled(_ context.Context) error { + destPath := filepath.Join(getCertDir(), certFileName) + if _, err := os.Stat(destPath); os.IsNotExist(err) { + return fmt.Errorf("proxy CA certificate not found at %s", destPath) + } + return nil +} + +func ClearProxyKeyring(ctx context.Context) error { + keyDescription := "keyring:safechain-proxy@tls-root-ca-key" + + output, err := exec.CommandContext(ctx, "keyctl", "search", "@s", "user", keyDescription).Output() + if err != nil { + return nil + } + + keyID := strings.TrimSpace(string(output)) + if keyID == "" { + return nil + } + + if err := exec.CommandContext(ctx, "keyctl", "invalidate", keyID).Run(); err != nil { + return fmt.Errorf("failed to invalidate keyring entry %s: %v", keyID, err) + } + + log.Printf("Cleared proxy keyring entry: %s (key ID: %s)", keyDescription, keyID) + return nil +} + +func UninstallProxyCA(ctx context.Context) error { + errs := []error{} + + destPath := filepath.Join(getCertDir(), certFileName) + if err := os.Remove(destPath); err != nil && !os.IsNotExist(err) { + errs = append(errs, fmt.Errorf("failed to remove certificate: %v", err)) + } + + if err := refreshCACertificates(); err != nil { + errs = append(errs, fmt.Errorf("failed to update ca certificates: %v", err)) + } + + if err := ClearProxyKeyring(ctx); err != nil { + errs = append(errs, fmt.Errorf("failed to clear proxy keyring: %v", err)) + } + + if len(errs) > 0 { + return fmt.Errorf("failed to uninstall proxy CA: %v", errs) + } + return nil +} + +func IsWindowsService() bool { + return false +} + +func RunAsWindowsService(runner ServiceRunner, serviceName string) error { + return nil +} + +func getLoggedInUser(ctx context.Context) (string, error) { + output, err := exec.CommandContext(ctx, "loginctl", "list-users", "--no-legend").Output() + if err != nil { + envUser := os.Getenv("SUDO_USER") + if envUser != "" { + return envUser, nil + } + return "", fmt.Errorf("failed to get logged in user: %v", err) + } + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) >= 2 && fields[1] != "root" { + return fields[1], nil + } + } + + envUser := os.Getenv("SUDO_USER") + if envUser != "" { + return envUser, nil + } + return "", fmt.Errorf("no interactive user found") +} + +func RunAsCurrentUser(ctx context.Context, binaryPath string, args []string) (string, error) { + if !RunningAsRoot() { + return utils.RunCommand(ctx, binaryPath, args...) + } + + username, err := getLoggedInUser(ctx) + if err != nil { + return "", fmt.Errorf("failed to get logged in user: %v", err) + } + + suArgs := append([]string{"-u", username, binaryPath}, args...) + return utils.RunCommandWithEnv(ctx, []string{}, "sudo", suArgs...) +} + +func RunInAuditSessionOfCurrentUser(ctx context.Context, binaryPath string, args []string) (string, error) { + return RunAsCurrentUser(ctx, binaryPath, args) +} + +func RunningAsRoot() bool { + return os.Getuid() == 0 +} + +func downloadSafeChainShellScript(ctx context.Context, repoURL, version string, scriptName string) (string, error) { + scriptURL := fmt.Sprintf("%s/releases/download/%s/%s", repoURL, version, scriptName) + scriptPath := filepath.Join(os.TempDir(), scriptName) + verification := utils.DownloadVerification{ + SafeChainReleaseTag: version, + SafeChainAssetName: scriptName, + } + + log.Printf("Downloading script %s from %s...", scriptName, scriptURL) + if err := utils.DownloadAndVerifyBinary(ctx, scriptURL, scriptPath, verification); err != nil { + return "", fmt.Errorf("failed to download script %s: %w", scriptName, err) + } + return scriptPath, nil +} + +func InstallSafeChain(ctx context.Context, repoURL, version string) error { + scriptPath, err := downloadSafeChainShellScript(ctx, repoURL, version, SafeChainInstallScriptName) + if err != nil { + return err + } + defer os.Remove(scriptPath) + if _, err := RunAsCurrentUser(ctx, "sh", []string{scriptPath}); err != nil { + return fmt.Errorf("failed to run install script: %w", err) + } + return nil +} + +func UninstallSafeChain(ctx context.Context, repoURL, version string) error { + scriptPath, err := downloadSafeChainShellScript(ctx, repoURL, version, SafeChainUninstallScriptName) + if err != nil { + return err + } + defer os.Remove(scriptPath) + + if _, err := RunAsCurrentUser(ctx, "sh", []string{scriptPath}); err != nil { + return fmt.Errorf("failed to run uninstall script: %w", err) + } + return nil +} diff --git a/internal/platform/platform_windows.go b/internal/platform/platform_windows.go index b2b94526..4cacc014 100644 --- a/internal/platform/platform_windows.go +++ b/internal/platform/platform_windows.go @@ -212,11 +212,6 @@ func UninstallProxyCA(ctx context.Context) error { return nil } -type ServiceRunner interface { - Start(ctx context.Context) error - Stop(ctx context.Context) error -} - type windowsService struct { runner ServiceRunner } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index df64b925..82a944a0 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -66,6 +66,8 @@ func FetchLatestVersion(ctx context.Context, repoURL, binaryName string) (string func DetectOS() (string, string) { switch runtime.GOOS { + case "linux": + return "linux", "" case "darwin": return "macos", "" case "windows": diff --git a/packaging/rpm/build-rpm.sh b/packaging/rpm/build-rpm.sh new file mode 100755 index 00000000..d6fbd715 --- /dev/null +++ b/packaging/rpm/build-rpm.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +set -e + +VERSION="" +ARCH="" +BIN_DIR="./bin" +OUTPUT_DIR="./dist" + +while getopts "v:a:b:o:h" opt; do + case $opt in + v) VERSION="$OPTARG" ;; + a) ARCH="$OPTARG" ;; + b) BIN_DIR="$OPTARG" ;; + o) OUTPUT_DIR="$OPTARG" ;; + h) + echo "Usage: $0 -v VERSION -a ARCH [-b BIN_DIR] [-o OUTPUT_DIR]" + echo " -v VERSION Version number (e.g., 1.0.0)" + echo " -a ARCH Architecture (arm64 or amd64)" + echo " -b BIN_DIR Binary directory (default: ./bin)" + echo " -o OUTPUT_DIR Output directory (default: ./dist)" + exit 0 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +if [ -z "$VERSION" ]; then + echo "Error: VERSION is required (-v)" >&2 + exit 1 +fi + +if [ -z "$ARCH" ]; then + echo "Error: ARCH is required (-a)" >&2 + exit 1 +fi + +if [ "$VERSION" = "dev" ]; then + PKG_VERSION="0.0.0" +else + PKG_VERSION="$VERSION" +fi + +case "$ARCH" in + amd64) RPM_ARCH="x86_64" ;; + arm64) RPM_ARCH="aarch64" ;; + *) + echo "Error: Unsupported architecture: $ARCH (expected amd64 or arm64)" >&2 + exit 1 + ;; +esac + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +BIN_DIR="$(cd "$BIN_DIR" 2>/dev/null && pwd || echo "$PROJECT_DIR/$BIN_DIR")" +OUTPUT_DIR="$(mkdir -p "$OUTPUT_DIR" && cd "$OUTPUT_DIR" && pwd)" + +echo "Building Linux RPM installer for SafeChain Ultimate v$VERSION" +echo " Architecture: $ARCH ($RPM_ARCH)" +echo " Binary directory: $BIN_DIR" +echo " Output directory: $OUTPUT_DIR" +echo " Project directory: $PROJECT_DIR" + +AGENT_BIN="$BIN_DIR/safechain-ultimate-linux-$ARCH" +AGENT_UI_BIN="$BIN_DIR/safechain-ultimate-ui-linux-$ARCH" +PROXY_BIN="$BIN_DIR/safechain-proxy-linux-$ARCH" + +if [ ! -f "$AGENT_BIN" ]; then + echo "Error: safechain-ultimate binary not found at $AGENT_BIN" >&2 + exit 1 +fi + +if [ ! -f "$AGENT_UI_BIN" ]; then + echo "Error: safechain-ultimate-ui binary not found at $AGENT_UI_BIN" >&2 + exit 1 +fi + +if [ ! -f "$PROXY_BIN" ]; then + echo "Error: safechain-proxy binary not found at $PROXY_BIN" >&2 + exit 1 +fi + +BUILD_DIR="$(mktemp -d)" +trap "rm -rf '$BUILD_DIR'" EXIT + +echo "Using temporary build directory: $BUILD_DIR" + +RPMBUILD_DIR="$BUILD_DIR/rpmbuild" +mkdir -p "$RPMBUILD_DIR"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + +echo "Copying binaries to SOURCES..." +cp "$AGENT_BIN" "$RPMBUILD_DIR/SOURCES/safechain-ultimate" +cp "$AGENT_UI_BIN" "$RPMBUILD_DIR/SOURCES/safechain-ultimate-ui" +cp "$PROXY_BIN" "$RPMBUILD_DIR/SOURCES/safechain-proxy" + +echo "Copying packaging files to SOURCES..." +cp "$SCRIPT_DIR/safechain-ultimate.service" "$RPMBUILD_DIR/SOURCES/" +cp "$SCRIPT_DIR/scripts/uninstall" "$RPMBUILD_DIR/SOURCES/" + +echo "Copying spec file..." +cp "$SCRIPT_DIR/safechain-ultimate.spec" "$RPMBUILD_DIR/SPECS/" + +echo "Building RPM package..." +rpmbuild -bb \ + --define "_topdir $RPMBUILD_DIR" \ + --define "_pkg_version $PKG_VERSION" \ + --target "$RPM_ARCH" \ + "$RPMBUILD_DIR/SPECS/safechain-ultimate.spec" + +RPM_FILE=$(find "$RPMBUILD_DIR/RPMS/$RPM_ARCH/" -name "safechain-ultimate-*.rpm" | head -1) + +if [ -z "$RPM_FILE" ]; then + echo "Error: RPM file not found after build" >&2 + exit 1 +fi + +OUTPUT_RPM="$OUTPUT_DIR/SafeChainUltimate-$VERSION-$ARCH.rpm" +cp "$RPM_FILE" "$OUTPUT_RPM" + +echo "" +echo "✓ RPM package built successfully: $OUTPUT_RPM" +echo "" + +CHECKSUM=$(sha256sum "$OUTPUT_RPM" | awk '{print $1}') +echo "SHA256: $CHECKSUM" +echo "$CHECKSUM" > "$OUTPUT_RPM.sha256" +echo "" + +echo "Package information:" +rpm -qip "$OUTPUT_RPM" +echo "" + +SIZE=$(du -h "$OUTPUT_RPM" | awk '{print $1}') +echo "Package size: $SIZE" + +exit 0 diff --git a/packaging/rpm/safechain-ultimate.service b/packaging/rpm/safechain-ultimate.service new file mode 100644 index 00000000..08505978 --- /dev/null +++ b/packaging/rpm/safechain-ultimate.service @@ -0,0 +1,16 @@ +[Unit] +Description=SafeChain Ultimate - Security Agent by Aikido Security +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/opt/aikidosecurity/safechainultimate/bin/safechain-ultimate +WorkingDirectory=/opt/aikidosecurity/safechainultimate +Restart=always +RestartSec=60 +StandardOutput=append:/var/log/aikidosecurity/safechainultimate/safechain-ultimate.log +StandardError=append:/var/log/aikidosecurity/safechainultimate/safechain-ultimate.error.log + +[Install] +WantedBy=multi-user.target diff --git a/packaging/rpm/safechain-ultimate.spec b/packaging/rpm/safechain-ultimate.spec new file mode 100644 index 00000000..228a77fc --- /dev/null +++ b/packaging/rpm/safechain-ultimate.spec @@ -0,0 +1,88 @@ +Name: safechain-ultimate +Version: %{_pkg_version} +Release: 1%{?dist} +Summary: SafeChain Ultimate - Security Agent by Aikido Security + +License: AGPL-3.0-or-later +URL: https://aikido.dev + +AutoReqProv: no + +%description +SafeChain Ultimate is a security agent by Aikido Security that protects +your applications and infrastructure. + +%install +mkdir -p %{buildroot}/opt/aikidosecurity/safechainultimate/bin +mkdir -p %{buildroot}/opt/aikidosecurity/safechainultimate/scripts +mkdir -p %{buildroot}/usr/lib/systemd/system +mkdir -p %{buildroot}/var/log/aikidosecurity/safechainultimate + +install -m 755 %{_sourcedir}/safechain-ultimate %{buildroot}/opt/aikidosecurity/safechainultimate/bin/ +install -m 755 %{_sourcedir}/safechain-ultimate-ui %{buildroot}/opt/aikidosecurity/safechainultimate/bin/ +install -m 755 %{_sourcedir}/safechain-proxy %{buildroot}/opt/aikidosecurity/safechainultimate/bin/ +install -m 755 %{_sourcedir}/uninstall %{buildroot}/opt/aikidosecurity/safechainultimate/scripts/ +install -m 644 %{_sourcedir}/safechain-ultimate.service %{buildroot}/usr/lib/systemd/system/ + +%files +%dir /opt/aikidosecurity +%dir /opt/aikidosecurity/safechainultimate +%dir /opt/aikidosecurity/safechainultimate/bin +%dir /opt/aikidosecurity/safechainultimate/scripts +%dir /var/log/aikidosecurity +%dir /var/log/aikidosecurity/safechainultimate +%attr(755, root, root) /opt/aikidosecurity/safechainultimate/bin/safechain-ultimate +%attr(755, root, root) /opt/aikidosecurity/safechainultimate/bin/safechain-ultimate-ui +%attr(755, root, root) /opt/aikidosecurity/safechainultimate/bin/safechain-proxy +%attr(755, root, root) /opt/aikidosecurity/safechainultimate/scripts/uninstall +%attr(644, root, root) /usr/lib/systemd/system/safechain-ultimate.service + +%pre +if pidof systemd &>/dev/null && systemctl is-active --quiet safechain-ultimate 2>/dev/null; then + systemctl stop safechain-ultimate || true +fi + +%post +if pidof systemd &>/dev/null; then + systemctl daemon-reload + systemctl enable safechain-ultimate + systemctl start safechain-ultimate + echo "" + echo "SafeChain Ultimate has been installed successfully!" + echo " Binaries: /opt/aikidosecurity/safechainultimate/bin" + echo " Logs: /var/log/aikidosecurity/safechainultimate" + echo "" + echo "The agent is now running as a systemd service." +else + /opt/aikidosecurity/safechainultimate/bin/safechain-ultimate \ + >>/var/log/aikidosecurity/safechainultimate/safechain-ultimate.log \ + 2>>/var/log/aikidosecurity/safechainultimate/safechain-ultimate.error.log & + echo "" + echo "SafeChain Ultimate has been installed successfully!" + echo " Binaries: /opt/aikidosecurity/safechainultimate/bin" + echo " Logs: /var/log/aikidosecurity/safechainultimate" + echo "" + echo "The agent is now running in the background (PID: $!)." +fi + +%preun +if [ $1 -eq 0 ]; then + if pidof systemd &>/dev/null; then + if systemctl is-active --quiet safechain-ultimate 2>/dev/null; then + systemctl stop safechain-ultimate || true + fi + systemctl disable safechain-ultimate 2>/dev/null || true + else + pkill -f /opt/aikidosecurity/safechainultimate/bin/safechain-ultimate || true + fi +fi + +%postun +if pidof systemd &>/dev/null; then + systemctl daemon-reload +fi +if [ $1 -eq 0 ]; then + rm -rf /var/log/aikidosecurity/safechainultimate + rmdir /var/log/aikidosecurity 2>/dev/null || true + rmdir /opt/aikidosecurity 2>/dev/null || true +fi diff --git a/packaging/rpm/scripts/uninstall b/packaging/rpm/scripts/uninstall new file mode 100755 index 00000000..f47eb3f2 --- /dev/null +++ b/packaging/rpm/scripts/uninstall @@ -0,0 +1,99 @@ +#!/bin/bash + +set -e + +INSTALL_DIR="/opt/aikidosecurity/safechainultimate" +LOGS_DIR="/var/log/aikidosecurity/safechainultimate" +SERVICE_FILE="/usr/lib/systemd/system/safechain-ultimate.service" +SERVICE_NAME="safechain-ultimate" +BINARY="$INSTALL_DIR/bin/safechain-ultimate" + +echo "=========================================" +echo "SafeChain Ultimate - Uninstaller" +echo "=========================================" +echo "" + +if [ "$(id -u)" -ne 0 ]; then + echo "Error: This script must be run as root (use sudo)" + exit 1 +fi + +echo "Stopping SafeChain Ultimate processes..." + +if pidof systemd &>/dev/null; then + if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then + echo " Stopping service..." + systemctl stop "$SERVICE_NAME" || true + echo " ✓ Service stopped" + else + echo " Service not running" + fi + + if systemctl is-enabled --quiet "$SERVICE_NAME" 2>/dev/null; then + echo " Disabling service..." + systemctl disable "$SERVICE_NAME" || true + echo " ✓ Service disabled" + fi +else + echo " systemd not detected, skipping service management" +fi + +pkill -f "safechain-ultimate-ui" 2>/dev/null && echo " ✓ UI process stopped" || echo " UI process not running" +pkill -f "safechain-proxy" 2>/dev/null && echo " ✓ Proxy process stopped" || echo " Proxy process not running" +pkill -f "safechain-ultimate" 2>/dev/null && echo " ✓ Daemon process stopped" || echo " Daemon process not running" + +echo "" + +if [ -f "$BINARY" ]; then + echo "Running teardown..." + "$BINARY" --teardown || { + echo " Warning: Teardown command failed (may already be torn down)" + } + echo " ✓ Teardown complete" +else + echo "Warning: Binary not found at $BINARY, skipping teardown" +fi + +echo "" +echo "Removing installed files..." + +if [ -f "$SERVICE_FILE" ]; then + rm -f "$SERVICE_FILE" + echo " ✓ Removed systemd service file" +fi + +if [ -d "$INSTALL_DIR" ]; then + rm -rf "$INSTALL_DIR" + echo " ✓ Removed application files" +fi + +if [ -d "$LOGS_DIR" ]; then + rm -rf "$LOGS_DIR" + echo " ✓ Removed log files" +fi + +AIKIDO_DIR="/opt/aikidosecurity" +if [ -d "$AIKIDO_DIR" ] && [ -z "$(ls -A "$AIKIDO_DIR" 2>/dev/null)" ]; then + rmdir "$AIKIDO_DIR" 2>/dev/null || true + echo " ✓ Removed empty AikidoSecurity directory" +fi + +AIKIDO_LOGS_DIR="/var/log/aikidosecurity" +if [ -d "$AIKIDO_LOGS_DIR" ] && [ -z "$(ls -A "$AIKIDO_LOGS_DIR" 2>/dev/null)" ]; then + rmdir "$AIKIDO_LOGS_DIR" 2>/dev/null || true + echo " ✓ Removed empty AikidoSecurity logs directory" +fi + +if pidof systemd &>/dev/null; then + echo "" + echo "Reloading systemd..." + systemctl daemon-reload +fi + +echo "" +echo "=========================================" +echo "✓ SafeChain Ultimate has been uninstalled" +echo "=========================================" +echo "" + +exit 0