660 lines
23 KiB
Go
660 lines
23 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
cryptorand "crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/big"
|
|
"math/rand/v2"
|
|
"net/http"
|
|
neturl "net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/cmd/osquery-perf/osquery_perf"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/android"
|
|
"github.com/google/uuid"
|
|
"google.golang.org/api/androidmanagement/v1"
|
|
)
|
|
|
|
// androidAgent simulates a single Android device for load testing.
|
|
// It communicates with Fleet via PubSub messages (enrollment, status reports, command acks)
|
|
// and coordinates with a mock AMAPI proxy to get policy versions and pending commands.
|
|
type androidAgent struct {
|
|
agentIndex int
|
|
serverAddress string
|
|
enrollSecret string
|
|
pubSubToken string
|
|
proxyAddress string
|
|
stats *osquery_perf.Stats
|
|
|
|
// Device identity (stable across the agent lifetime)
|
|
enterpriseSpecificID string
|
|
serialNumber string
|
|
deviceName string // AMAPI resource name: enterprises/{id}/devices/{id}
|
|
enterpriseID string
|
|
orbitNodeKey string // obtained from orbit enrollment, used for certificate API auth
|
|
|
|
// Hardware details
|
|
brand string
|
|
model string
|
|
hardware string
|
|
|
|
// Software
|
|
androidVersion string
|
|
androidBuildNumber string
|
|
|
|
// Memory
|
|
totalRAM int64
|
|
totalInternalStorage int64
|
|
|
|
// Installed apps reported in STATUS_REPORT
|
|
installedApps []*androidmanagement.ApplicationReport
|
|
|
|
// Timing
|
|
statusReportInterval time.Duration
|
|
|
|
// Non-compliance probability (fraction of STATUS_REPORTs that include non-compliance details)
|
|
nonComplianceProb float64
|
|
}
|
|
|
|
// androidApp is a simplified app definition for generating realistic ApplicationReports.
|
|
var androidApps = []struct {
|
|
displayName string
|
|
packageName string
|
|
baseVersion string
|
|
}{
|
|
{"Google Chrome", "com.android.chrome", "126.0.6478.122"},
|
|
{"Gmail", "com.google.android.gm", "2024.06.30.649015803"},
|
|
{"Google Maps", "com.google.android.apps.maps", "11.125.0102"},
|
|
{"YouTube", "com.google.android.youtube", "19.25.33"},
|
|
{"Google Drive", "com.google.android.apps.docs", "2.24.277.0"},
|
|
{"Google Photos", "com.google.android.apps.photos", "7.1.0.611579560"},
|
|
{"Google Calendar", "com.google.android.calendar", "2024.25.0-647498253"},
|
|
{"Google Meet", "com.google.android.apps.tachyon", "2024.06.30.643793517"},
|
|
{"Slack", "com.Slack", "24.06.10.0"},
|
|
{"Microsoft Teams", "com.microsoft.teams", "1416/1.0.0.2024063002"},
|
|
{"Microsoft Outlook", "com.microsoft.office.outlook", "4.2425.1"},
|
|
{"Zoom", "us.zoom.videomeetings", "6.1.1.21782"},
|
|
{"Salesforce", "com.salesforce.chatter", "246.010.0"},
|
|
{"1Password", "com.onepassword.android", "8.10.38"},
|
|
{"Authenticator", "com.google.android.apps.authenticator2", "7.0"},
|
|
{"Google Docs", "com.google.android.apps.docs.editors.docs", "1.24.272.01"},
|
|
{"Google Sheets", "com.google.android.apps.docs.editors.sheets", "1.24.272.01"},
|
|
{"Google Slides", "com.google.android.apps.docs.editors.slides", "1.24.272.01"},
|
|
{"Google Keep", "com.google.android.keep", "5.24.272.00"},
|
|
{"Google Messages", "com.google.android.apps.messaging", "20240625"},
|
|
{"Files by Google", "com.google.android.apps.nbu.files", "1.4396.621459950"},
|
|
{"Google Phone", "com.google.android.dialer", "130.0.631022283"},
|
|
{"Google Contacts", "com.google.android.contacts", "4.32.33.621636488"},
|
|
{"Google Clock", "com.google.android.deskclock", "7.8"},
|
|
{"Google Calculator", "com.google.android.calculator", "8.8"},
|
|
{"Google Camera", "com.google.android.GoogleCamera", "9.3.160.621982096"},
|
|
{"Google Play Store", "com.android.vending", "41.6.26"},
|
|
{"Google Play Services", "com.google.android.gms", "24.26.14"},
|
|
{"Android System WebView", "com.google.android.webview", "126.0.6478.122"},
|
|
{"Google Translate", "com.google.android.apps.translate", "8.7.29.626714160"},
|
|
{"LinkedIn", "com.linkedin.android", "4.1.972"},
|
|
{"Spotify", "com.spotify.music", "8.9.42.575"},
|
|
{"WhatsApp", "com.whatsapp", "2.24.14.78"},
|
|
{"Signal", "org.thoughtcrime.securesms", "7.11.3"},
|
|
{"Firefox", "org.mozilla.firefox", "127.0.2"},
|
|
{"Adobe Acrobat", "com.adobe.reader", "24.6.0.33768"},
|
|
{"Dropbox", "com.dropbox.android", "372.2.2"},
|
|
{"Evernote", "com.evernote", "10.95"},
|
|
{"Trello", "com.trello", "2024.10"},
|
|
{"Notion", "notion.id", "0.6.2413"},
|
|
{"GitHub", "com.github.android", "1.148.0"},
|
|
{"Jira Cloud", "com.atlassian.android.jira.core", "2024.06.30"},
|
|
{"Okta Verify", "com.okta.android.auth", "9.6.1"},
|
|
{"Duo Mobile", "com.duosecurity.duomobile", "4.62.0"},
|
|
{"CrowdStrike Falcon", "com.crowdstrike.android.falcon", "7.19.17004"},
|
|
{"Intune Company Portal", "com.microsoft.windowsintune.companyportal", "5.0.6233.0"},
|
|
{"Fleet Agent", "com.fleetdm.agent", "1.3.0"},
|
|
{"Samsung Knox", "com.samsung.android.knox.containercore", "2.7.1"},
|
|
{"Google Admin", "com.google.android.apps.enterprise.cpanel", "2024.06.30.627"},
|
|
{"LastPass", "com.lastpass.lpandroid", "5.21.0.13562"},
|
|
}
|
|
|
|
// newAndroidAgent creates a new Android device simulator.
|
|
func newAndroidAgent(
|
|
agentIndex int,
|
|
serverAddress string,
|
|
enrollSecret string,
|
|
pubSubToken string,
|
|
proxyAddress string,
|
|
enterpriseID string,
|
|
statusReportInterval time.Duration,
|
|
appCount int,
|
|
nonComplianceProb float64,
|
|
stats *osquery_perf.Stats,
|
|
) *androidAgent {
|
|
enterpriseSpecificID := strings.ToUpper(uuid.New().String())
|
|
deviceID := "fake" + strings.ReplaceAll(uuid.New().String()[:28], "-", "")
|
|
serialNumber := fmt.Sprintf("AND%s", randomString(10))
|
|
|
|
brands := []string{"Google", "Samsung", "OnePlus", "Motorola", "Nokia"}
|
|
models := []string{"Pixel 8 Pro", "Pixel 7a", "Galaxy S24", "Galaxy A54", "Nord CE 3", "Edge 40", "X30"}
|
|
hardwareTypes := []string{"qcom", "exynos", "tensor", "dimensity"}
|
|
|
|
brand := brands[rand.IntN(len(brands))] // #nosec G404 -- load testing only
|
|
model := models[rand.IntN(len(models))] // #nosec G404 -- load testing only
|
|
hardware := hardwareTypes[rand.IntN(len(hardwareTypes))] // #nosec G404 -- load testing only
|
|
|
|
// Android versions 13-15
|
|
androidVersions := []string{"13", "14", "15"}
|
|
androidVersion := androidVersions[rand.IntN(len(androidVersions))] // #nosec G404 -- load testing only
|
|
buildNumber := fmt.Sprintf("TP1A.%d%02d%02d.003", 2024+rand.IntN(2), 1+rand.IntN(12), 1+rand.IntN(28)) // #nosec G404 -- load testing only
|
|
|
|
// Generate installed apps list
|
|
if appCount > len(androidApps) {
|
|
appCount = len(androidApps)
|
|
}
|
|
// Shuffle and pick appCount apps
|
|
perm := rand.Perm(len(androidApps))
|
|
apps := make([]*androidmanagement.ApplicationReport, 0, appCount)
|
|
for i := 0; i < appCount; i++ {
|
|
app := androidApps[perm[i]]
|
|
apps = append(apps, &androidmanagement.ApplicationReport{
|
|
DisplayName: app.displayName,
|
|
PackageName: app.packageName,
|
|
VersionName: app.baseVersion,
|
|
State: "INSTALLED",
|
|
})
|
|
}
|
|
|
|
// Memory: 4-12 GB RAM, 64-256 GB storage
|
|
ramOptions := []int64{4, 6, 8, 12}
|
|
storageOptions := []int64{64, 128, 256}
|
|
totalRAM := ramOptions[rand.IntN(len(ramOptions))] * 1024 * 1024 * 1024 // #nosec G404 -- load testing only
|
|
totalStorage := storageOptions[rand.IntN(len(storageOptions))] * 1024 * 1024 * 1024 // #nosec G404 -- load testing only
|
|
|
|
return &androidAgent{
|
|
agentIndex: agentIndex,
|
|
serverAddress: serverAddress,
|
|
enrollSecret: enrollSecret,
|
|
pubSubToken: pubSubToken,
|
|
proxyAddress: proxyAddress,
|
|
enterpriseID: enterpriseID,
|
|
stats: stats,
|
|
enterpriseSpecificID: enterpriseSpecificID,
|
|
serialNumber: serialNumber,
|
|
deviceName: fmt.Sprintf("enterprises/%s/devices/%s", enterpriseID, deviceID),
|
|
brand: brand,
|
|
model: model,
|
|
hardware: hardware,
|
|
androidVersion: androidVersion,
|
|
androidBuildNumber: buildNumber,
|
|
totalRAM: totalRAM,
|
|
totalInternalStorage: totalStorage,
|
|
installedApps: apps,
|
|
statusReportInterval: statusReportInterval,
|
|
nonComplianceProb: nonComplianceProb,
|
|
}
|
|
}
|
|
|
|
// runLoop is the main loop for the Android agent.
|
|
// It registers with the mock proxy, sends enrollment to Fleet, then periodically sends status reports.
|
|
func (a *androidAgent) runLoop() {
|
|
// Step 1: Register with mock AMAPI proxy (retry with backoff)
|
|
for attempt := 1; ; attempt++ {
|
|
if err := a.registerWithProxy(); err != nil {
|
|
if attempt >= 5 {
|
|
log.Printf("Android agent %d: failed to register with proxy after %d attempts: %v", a.agentIndex, attempt, err)
|
|
return
|
|
}
|
|
log.Printf("Android agent %d: register attempt %d failed, retrying: %v", a.agentIndex, attempt, err)
|
|
time.Sleep(time.Duration(attempt) * 5 * time.Second)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
// Step 2: Send ENROLLMENT PubSub to Fleet (retry with backoff)
|
|
for attempt := 1; ; attempt++ {
|
|
if err := a.sendEnrollment(); err != nil {
|
|
if attempt >= 5 {
|
|
log.Printf("Android agent %d: enrollment failed after %d attempts: %v", a.agentIndex, attempt, err)
|
|
return
|
|
}
|
|
log.Printf("Android agent %d: enrollment attempt %d failed, retrying: %v", a.agentIndex, attempt, err)
|
|
time.Sleep(time.Duration(attempt) * 5 * time.Second)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
a.stats.IncrementAndroidEnrollments()
|
|
|
|
// Step 2b: Orbit enrollment (retry with backoff, non-fatal)
|
|
for attempt := 1; ; attempt++ {
|
|
if err := a.orbitEnroll(); err != nil {
|
|
if attempt >= 3 {
|
|
log.Printf("Android agent %d: orbit enrollment failed after %d attempts: %v", a.agentIndex, attempt, err)
|
|
break // Non-fatal — certificate flow won't work but status reports will
|
|
}
|
|
log.Printf("Android agent %d: orbit enrollment attempt %d failed, retrying: %v", a.agentIndex, attempt, err)
|
|
time.Sleep(time.Duration(attempt) * 5 * time.Second)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
// Step 3: Periodic status reports + command ack + certificate verification loop
|
|
statusTicker := time.NewTicker(a.statusReportInterval)
|
|
defer statusTicker.Stop()
|
|
|
|
// Track which certificate templates we've already verified so we don't re-verify
|
|
verifiedCerts := make(map[uint]struct{})
|
|
|
|
for range statusTicker.C {
|
|
// Poll proxy for current state (policy version, pending commands)
|
|
state, err := a.pollProxyState()
|
|
if err != nil {
|
|
log.Printf("Android agent %d: failed to poll proxy: %v", a.agentIndex, err)
|
|
a.stats.IncrementAndroidErrors()
|
|
continue
|
|
}
|
|
|
|
// Send STATUS_REPORT
|
|
if err := a.sendStatusReport(state); err != nil {
|
|
log.Printf("Android agent %d: status report failed: %v", a.agentIndex, err)
|
|
a.stats.IncrementAndroidErrors()
|
|
continue
|
|
}
|
|
a.stats.IncrementAndroidStatusReports()
|
|
|
|
// Ack any pending commands
|
|
for _, opName := range state.PendingCommands {
|
|
if err := a.sendCommandAck(opName); err != nil {
|
|
log.Printf("Android agent %d: command ack failed for %s: %v", a.agentIndex, opName, err)
|
|
a.stats.IncrementAndroidErrors()
|
|
continue
|
|
}
|
|
a.stats.IncrementAndroidCommandAcks()
|
|
}
|
|
|
|
// Process certificate templates from the proxy state.
|
|
if a.orbitNodeKey != "" {
|
|
for _, certID := range state.PendingCertificates {
|
|
// Always GET the cert to check its current status — renewals reuse the same template ID
|
|
cert, err := a.getCertificateTemplate(certID)
|
|
if err != nil {
|
|
log.Printf("Android agent %d: get certificate %d failed: %v", a.agentIndex, certID, err)
|
|
a.stats.IncrementAndroidErrors()
|
|
continue
|
|
}
|
|
|
|
// If status went back to non-delivered (renewal in progress), clear our tracking
|
|
if cert.Status != "delivered" {
|
|
delete(verifiedCerts, certID)
|
|
continue
|
|
}
|
|
|
|
// Skip if we already verified this delivery
|
|
if _, ok := verifiedCerts[certID]; ok {
|
|
continue
|
|
}
|
|
|
|
// PUT the certificate status as verified (simulates SCEP enrollment completion)
|
|
if err := a.updateCertificateStatus(certID, "verified", "install"); err != nil {
|
|
log.Printf("Android agent %d: update certificate %d status failed: %v", a.agentIndex, certID, err)
|
|
a.stats.IncrementAndroidErrors()
|
|
continue
|
|
}
|
|
|
|
verifiedCerts[certID] = struct{}{}
|
|
a.stats.IncrementAndroidCertVerifications()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// proxyDeviceState is the response from the mock proxy's coordination API.
|
|
type proxyDeviceState struct {
|
|
PolicyVersion int64 `json:"policy_version"`
|
|
PolicyName string `json:"policy_name"`
|
|
PendingCommands []string `json:"pending_commands"`
|
|
PendingCertificates []uint `json:"pending_certificates"`
|
|
}
|
|
|
|
// certTemplateResponse is the response from GET /api/fleetd/certificates/{id}
|
|
type certTemplateResponse struct {
|
|
Certificate *certTemplateInfo `json:"certificate"`
|
|
}
|
|
|
|
type certTemplateInfo struct {
|
|
ID uint `json:"id"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// registerWithProxy registers this fake device with the mock AMAPI proxy.
|
|
func (a *androidAgent) registerWithProxy() error {
|
|
body := struct {
|
|
EnterpriseSpecificID string `json:"enterprise_specific_id"`
|
|
DeviceName string `json:"device_name"`
|
|
EnterpriseID string `json:"enterprise_id"`
|
|
}{
|
|
EnterpriseSpecificID: a.enterpriseSpecificID,
|
|
DeviceName: a.deviceName,
|
|
EnterpriseID: a.enterpriseID,
|
|
}
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal register body: %w", err)
|
|
}
|
|
|
|
resp, err := http.Post(a.proxyAddress+"/mock/devices/register", "application/json", bytes.NewReader(data))
|
|
if err != nil {
|
|
return fmt.Errorf("register request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("register returned %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pollProxyState asks the mock proxy for the current state this device should report.
|
|
func (a *androidAgent) pollProxyState() (*proxyDeviceState, error) {
|
|
resp, err := http.Get(a.proxyAddress + "/mock/devices/" + a.enterpriseSpecificID + "/state")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("poll state request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("poll state returned %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var state proxyDeviceState
|
|
if err := json.NewDecoder(resp.Body).Decode(&state); err != nil {
|
|
return nil, fmt.Errorf("decode state: %w", err)
|
|
}
|
|
return &state, nil
|
|
}
|
|
|
|
// sendEnrollment sends an ENROLLMENT PubSub message to Fleet.
|
|
func (a *androidAgent) sendEnrollment() error {
|
|
device := androidmanagement.Device{
|
|
Name: a.deviceName,
|
|
Ownership: "COMPANY_OWNED",
|
|
EnrollmentTokenData: fmt.Sprintf(`{"EnrollSecret": "%s"}`, a.enrollSecret),
|
|
HardwareInfo: &androidmanagement.HardwareInfo{
|
|
EnterpriseSpecificId: a.enterpriseSpecificID,
|
|
SerialNumber: a.serialNumber,
|
|
Brand: a.brand,
|
|
Model: a.model,
|
|
Hardware: a.hardware,
|
|
},
|
|
SoftwareInfo: &androidmanagement.SoftwareInfo{
|
|
AndroidVersion: a.androidVersion,
|
|
AndroidBuildNumber: a.androidBuildNumber,
|
|
},
|
|
MemoryInfo: &androidmanagement.MemoryInfo{
|
|
TotalRam: a.totalRAM,
|
|
TotalInternalStorage: a.totalInternalStorage,
|
|
},
|
|
MemoryEvents: a.generateMemoryEvents(),
|
|
}
|
|
|
|
return a.sendPubSubMessage(android.PubSubEnrollment, device)
|
|
}
|
|
|
|
// sendStatusReport sends a STATUS_REPORT PubSub message to Fleet.
|
|
func (a *androidAgent) sendStatusReport(state *proxyDeviceState) error {
|
|
now := time.Now().UTC()
|
|
|
|
device := androidmanagement.Device{
|
|
Name: a.deviceName,
|
|
Ownership: "COMPANY_OWNED",
|
|
AppliedState: "ACTIVE",
|
|
HardwareInfo: &androidmanagement.HardwareInfo{
|
|
EnterpriseSpecificId: a.enterpriseSpecificID,
|
|
SerialNumber: a.serialNumber,
|
|
Brand: a.brand,
|
|
Model: a.model,
|
|
Hardware: a.hardware,
|
|
},
|
|
SoftwareInfo: &androidmanagement.SoftwareInfo{
|
|
AndroidVersion: a.androidVersion,
|
|
AndroidBuildNumber: a.androidBuildNumber,
|
|
},
|
|
MemoryInfo: &androidmanagement.MemoryInfo{
|
|
TotalRam: a.totalRAM,
|
|
TotalInternalStorage: a.totalInternalStorage,
|
|
},
|
|
MemoryEvents: a.generateMemoryEvents(),
|
|
ApplicationReports: a.installedApps,
|
|
AppliedPolicyVersion: state.PolicyVersion,
|
|
AppliedPolicyName: state.PolicyName,
|
|
LastPolicySyncTime: now.Format(time.RFC3339),
|
|
LastStatusReportTime: now.Format(time.RFC3339),
|
|
EnrollmentTokenData: fmt.Sprintf(`{"EnrollSecret": "%s"}`, a.enrollSecret),
|
|
}
|
|
|
|
// Optionally add non-compliance details
|
|
nonCompliant := rand.Float64() < a.nonComplianceProb // #nosec G404 -- load testing only
|
|
if nonCompliant {
|
|
device.NonComplianceDetails = []*androidmanagement.NonComplianceDetail{
|
|
{
|
|
SettingName: "passwordPolicies",
|
|
NonComplianceReason: "USER_ACTION",
|
|
InstallationFailureReason: "",
|
|
},
|
|
}
|
|
}
|
|
|
|
return a.sendPubSubMessage(android.PubSubStatusReport, device)
|
|
}
|
|
|
|
// sendCommandAck sends a COMMAND PubSub message to Fleet acknowledging a completed command.
|
|
func (a *androidAgent) sendCommandAck(operationName string) error {
|
|
op := androidmanagement.Operation{
|
|
Name: operationName,
|
|
Done: true,
|
|
}
|
|
return a.sendPubSubMessage(android.PubSubCommand, op)
|
|
}
|
|
|
|
func (a *androidAgent) orbitEnroll() error {
|
|
body := struct {
|
|
EnrollSecret string `json:"enroll_secret"`
|
|
HardwareUUID string `json:"hardware_uuid"`
|
|
HardwareSerial string `json:"hardware_serial"`
|
|
Platform string `json:"platform"`
|
|
ComputerName string `json:"computer_name"`
|
|
}{
|
|
EnrollSecret: a.enrollSecret,
|
|
HardwareUUID: a.enterpriseSpecificID,
|
|
HardwareSerial: a.serialNumber,
|
|
Platform: "android",
|
|
ComputerName: fmt.Sprintf("%s %s", a.brand, a.model),
|
|
}
|
|
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal orbit enroll: %w", err)
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/api/fleet/orbit/enroll", a.serverAddress)
|
|
resp, err := http.Post(url, "application/json", bytes.NewReader(data)) // #nosec G107 -- load testing
|
|
if err != nil {
|
|
return fmt.Errorf("orbit enroll request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("orbit enroll returned %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var enrollResp struct {
|
|
OrbitNodeKey string `json:"orbit_node_key"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&enrollResp); err != nil {
|
|
return fmt.Errorf("decode orbit enroll response: %w", err)
|
|
}
|
|
if enrollResp.OrbitNodeKey == "" {
|
|
return errors.New("empty orbit_node_key in response")
|
|
}
|
|
|
|
a.orbitNodeKey = enrollResp.OrbitNodeKey
|
|
return nil
|
|
}
|
|
|
|
// getCertificateTemplate fetches a certificate template from Fleet.
|
|
func (a *androidAgent) getCertificateTemplate(certID uint) (*certTemplateInfo, error) {
|
|
url := fmt.Sprintf("%s/api/fleetd/certificates/%d", a.serverAddress, certID)
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Authorization", "Node key "+a.orbitNodeKey)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get certificate: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("get certificate returned %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var certResp certTemplateResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&certResp); err != nil {
|
|
return nil, fmt.Errorf("decode certificate: %w", err)
|
|
}
|
|
return certResp.Certificate, nil
|
|
}
|
|
|
|
// updateCertificateStatus reports a certificate template's status to Fleet.
|
|
func (a *androidAgent) updateCertificateStatus(certID uint, status, operationType string) error {
|
|
now := time.Now().UTC()
|
|
notBefore := now.Add(-1 * time.Hour)
|
|
notAfter := now.Add(365 * 24 * time.Hour)
|
|
|
|
// Generate a random serial number
|
|
serialBytes := make([]byte, 16)
|
|
if _, err := cryptorand.Read(serialBytes); err != nil {
|
|
return fmt.Errorf("generate serial: %w", err)
|
|
}
|
|
serial := new(big.Int).SetBytes(serialBytes).Text(16)
|
|
|
|
body := struct {
|
|
Status string `json:"status"`
|
|
OperationType string `json:"operation_type"`
|
|
NotValidBefore *time.Time `json:"not_valid_before"`
|
|
NotValidAfter *time.Time `json:"not_valid_after"`
|
|
Serial *string `json:"serial"`
|
|
}{
|
|
Status: status,
|
|
OperationType: operationType,
|
|
NotValidBefore: ¬Before,
|
|
NotValidAfter: ¬After,
|
|
Serial: &serial,
|
|
}
|
|
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal status: %w", err)
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/api/fleetd/certificates/%d/status", a.serverAddress, certID)
|
|
req, err := http.NewRequest("PUT", url, bytes.NewReader(data))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Authorization", "Node key "+a.orbitNodeKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("update certificate status: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("update certificate status returned %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sendPubSubMessage constructs and sends a PubSub push message to Fleet's endpoint.
|
|
func (a *androidAgent) sendPubSubMessage(notificationType android.NotificationType, payload any) error {
|
|
data, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal payload: %w", err)
|
|
}
|
|
|
|
encodedData := base64.StdEncoding.EncodeToString(data)
|
|
|
|
msg := struct {
|
|
Message android.PubSubMessage `json:"message"`
|
|
}{
|
|
Message: android.PubSubMessage{
|
|
Attributes: map[string]string{
|
|
"notificationType": string(notificationType),
|
|
},
|
|
Data: encodedData,
|
|
},
|
|
}
|
|
|
|
body, err := json.Marshal(msg)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal pubsub message: %w", err)
|
|
}
|
|
|
|
// POST to Fleet's PubSub endpoint with the token as a query parameter
|
|
url := fmt.Sprintf("%s/api/v1/fleet/android_enterprise/pubsub?token=%s", a.serverAddress, neturl.QueryEscape(a.pubSubToken))
|
|
resp, err := http.Post(url, "application/json", bytes.NewReader(body)) // #nosec G107 -- URL is constructed from trusted config
|
|
if err != nil {
|
|
return fmt.Errorf("pubsub POST: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("pubsub returned %d: %s", resp.StatusCode, string(respBody))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// generateMemoryEvents creates realistic memory events for the device.
|
|
func (a *androidAgent) generateMemoryEvents() []*androidmanagement.MemoryEvent {
|
|
now := time.Now().UTC()
|
|
// External storage = half of internal for simplicity
|
|
externalTotal := a.totalInternalStorage / 2
|
|
// Available = 30-80% of total
|
|
internalAvail := int64(float64(a.totalInternalStorage) * (0.3 + rand.Float64()*0.5)) // #nosec G404 -- load testing only
|
|
externalAvail := int64(float64(externalTotal) * (0.3 + rand.Float64()*0.5)) // #nosec G404 -- load testing only
|
|
|
|
return []*androidmanagement.MemoryEvent{
|
|
{
|
|
EventType: "EXTERNAL_STORAGE_DETECTED",
|
|
ByteCount: externalTotal,
|
|
CreateTime: now.Add(-24 * time.Hour).Format(time.RFC3339),
|
|
},
|
|
{
|
|
EventType: "INTERNAL_STORAGE_MEASURED",
|
|
ByteCount: internalAvail,
|
|
CreateTime: now.Format(time.RFC3339),
|
|
},
|
|
{
|
|
EventType: "EXTERNAL_STORAGE_MEASURED",
|
|
ByteCount: externalAvail,
|
|
CreateTime: now.Format(time.RFC3339),
|
|
},
|
|
}
|
|
}
|