)}
- {installedVersions.map((installedVersion) => {
+ {installedVersions.map((installedVersion, index) => {
return (
0 && stringContains(s, sub) {
+ return true
+ }
+ }
+ return false
+}
+
+func stringContains(s, sub string) bool {
+ return len(s) >= len(sub) && indexOf(s, sub) >= 0
+}
+
+func indexOf(s, sub string) int {
+ n := len(sub)
+ if n == 0 {
+ return 0
+ }
+ for i := 0; i+n <= len(s); i++ {
+ if s[i:i+n] == sub {
+ return i
+ }
+ }
+ return -1
+}
+
// Create the goose_db_version table
// and insert the initial 0 value into it
func (c *Client) createVersionTable(db *sql.DB) error {
diff --git a/server/logging/pubsub.go b/server/logging/pubsub.go
index 6f5b19fedc8..f196ee854fd 100644
--- a/server/logging/pubsub.go
+++ b/server/logging/pubsub.go
@@ -78,9 +78,13 @@ func (w *pubSubLogWriter) Write(ctx context.Context, logs []json.RawMessage) err
}
if len(data)+estimateAttributeSize(attributes) > pubsub.MaxPublishRequestBytes {
+ truncateLen := 100
+ if len(log) < truncateLen {
+ truncateLen = len(log)
+ }
w.logger.InfoContext(ctx, "dropping log over 10MB PubSub limit",
"size", len(data),
- "log", string(log[:100])+"...",
+ "log", string(log[:truncateLen])+"...",
)
continue
}
@@ -95,6 +99,9 @@ func (w *pubSubLogWriter) Write(ctx context.Context, logs []json.RawMessage) err
// Wait for each message to be pushed to the server
for _, result := range results {
+ if result == nil {
+ continue
+ }
_, err := result.Get(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "pubsub publish")
diff --git a/server/mdm/nanomdm/http/mdm/mdm.go b/server/mdm/nanomdm/http/mdm/mdm.go
index f6f620bf67c..c3c113290e2 100644
--- a/server/mdm/nanomdm/http/mdm/mdm.go
+++ b/server/mdm/nanomdm/http/mdm/mdm.go
@@ -57,6 +57,7 @@ func CheckinHandler(svc service.Checkin, logger log.Logger) http.HandlerFunc {
httpStatus = statusErr.Status
}
http.Error(w, http.StatusText(httpStatus), httpStatus)
+ return
}
_, _ = w.Write(respBytes)
}
@@ -86,6 +87,7 @@ func CommandAndReportResultsHandler(svc service.CommandAndReportResults, logger
httpStatus = statusErr.Status
}
http.Error(w, http.StatusText(httpStatus), httpStatus)
+ return
}
_, _ = w.Write(respBytes)
}
diff --git a/server/mdm/scep/depot/file/depot.go b/server/mdm/scep/depot/file/depot.go
index a4a9874f18f..81e89cb4e10 100644
--- a/server/mdm/scep/depot/file/depot.go
+++ b/server/mdm/scep/depot/file/depot.go
@@ -11,6 +11,7 @@ import (
"fmt"
"io"
"io/ioutil"
+ "log/slog"
"math/big"
"os"
"path/filepath"
@@ -26,7 +27,7 @@ func NewFileDepot(path string) (*fileDepot, error) {
fmt.Sprintf("%s/index.txt", path),
os.O_RDONLY|os.O_CREATE, 0o666)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("opening index.txt: %w", err)
}
defer f.Close()
return &fileDepot{dirPath: path}, nil
@@ -41,19 +42,19 @@ type fileDepot struct {
func (d *fileDepot) CA(pass []byte) ([]*x509.Certificate, *rsa.PrivateKey, error) {
caPEM, err := d.getFile("ca.pem")
if err != nil {
- return nil, nil, err
+ return nil, nil, fmt.Errorf("reading ca.pem: %w", err)
}
cert, err := loadCert(caPEM.Data)
if err != nil {
- return nil, nil, err
+ return nil, nil, fmt.Errorf("loading ca certificate: %w", err)
}
keyPEM, err := d.getFile("ca.key")
if err != nil {
- return nil, nil, err
+ return nil, nil, fmt.Errorf("reading ca.key: %w", err)
}
key, err := loadKey(keyPEM.Data, pass)
if err != nil {
- return nil, nil, err
+ return nil, nil, fmt.Errorf("loading ca key: %w", err)
}
return []*x509.Certificate{cert}, key, nil
}
@@ -100,7 +101,11 @@ func (d *fileDepot) Put(cn string, crt *x509.Certificate) error {
return err
}
if err := d.writeDB(cn, serial, filename, crt); err != nil {
- // TODO : remove certificate in case of writeDB problems
+ // remove the certificate file we just wrote so the on-disk PEM files
+ // stay in sync with index.txt; otherwise a leftover cert file with
+ // no corresponding index entry will desync the depot and cause a
+ // subsequent Put() with the same cn/serial to fail with "file exists".
+ os.Remove(filepath)
return err
}
@@ -115,19 +120,19 @@ func (d *fileDepot) Serial() (*big.Int, error) {
if err := d.check("serial"); err != nil {
// assuming it doesnt exist, create
if err := d.writeSerial(s); err != nil {
- return nil, err
+ return nil, fmt.Errorf("writing serial: %w", err)
}
return s, nil
}
file, err := os.Open(name)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("opening serial file: %w", err)
}
defer file.Close()
r := bufio.NewReader(file)
- data, err := r.ReadString('\r')
+ data, err := r.ReadString('\n')
if err != nil && err != io.EOF {
- return nil, err
+ return nil, fmt.Errorf("reading serial file: %w", err)
}
data = strings.TrimSuffix(data, "\r")
data = strings.TrimSuffix(data, "\n")
@@ -136,7 +141,7 @@ func (d *fileDepot) Serial() (*big.Int, error) {
return nil, errors.New("could not convert " + data + " to serial number")
}
if err := d.incrementSerial(serial); err != nil {
- return serial, err
+ return serial, fmt.Errorf("incrementing serial: %w", err)
}
return serial, nil
}
@@ -236,7 +241,7 @@ func (d *fileDepot) HasCN(_ string, allowTime int, cert *x509.Certificate, revok
return false, errors.New("DN " + dn + " already exists")
}
if revokeOldCertificate {
- fmt.Println("Revoking certificate with serial " + key + " from DB. Recreation of CRL needed.")
+ slog.Info("revoking certificate from DB, recreation of CRL needed", "serial", key)
entries := strings.Split(value, "\t")
addDB.WriteString("R\t" + entries[1] + "\t" + makeOpenSSLTime(time.Now()) + "\t" + strings.ToUpper(entries[3]) + "\t" + entries[4] + "\t" + entries[5] + "\n")
}
@@ -354,14 +359,17 @@ func (d *fileDepot) check(path string) error {
func (d *fileDepot) getFile(path string) (*file, error) {
if err := d.check(path); err != nil {
- return nil, err
+ return nil, fmt.Errorf("checking %s: %w", path, err)
}
fi, err := os.Stat(d.path(path))
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("stat %s: %w", path, err)
}
b, err := ioutil.ReadFile(d.path(path))
- return &file{fi, b}, err
+ if err != nil {
+ return nil, fmt.Errorf("reading %s: %w", path, err)
+ }
+ return &file{fi, b}, nil
}
func (d *fileDepot) path(name string) string {
diff --git a/server/service/async/collect.go b/server/service/async/collect.go
index ed4cccbb56a..5899a353e8c 100644
--- a/server/service/async/collect.go
+++ b/server/service/async/collect.go
@@ -106,7 +106,13 @@ func (c *collector) exec(ctx context.Context) {
c.addSkipStats(failed)
return
}
- defer conn.Do("DEL", keyLock) //nolint:errcheck
+ defer func() {
+ if _, err := conn.Do("DEL", keyLock); err != nil {
+ if c.errHandler != nil {
+ c.errHandler(c.name, err)
+ }
+ }
+ }()
// at this point, the lock has been acquired, execute the collector handler
ctx, cancel := context.WithTimeout(ctx, time.Duration(c.lockTimeout.Seconds())*time.Second)
@@ -179,9 +185,11 @@ func (c *collector) nextRunAfter() time.Duration {
var jitter time.Duration
if c.jitterPct > 0 {
maxJitter := time.Duration(c.jitterPct) * c.execInterval / time.Duration(100.0)
- randDuration, err := rand.Int(rand.Reader, big.NewInt(int64(maxJitter)))
- if err == nil {
- jitter = time.Duration(randDuration.Int64())
+ if maxJitter > 0 {
+ randDuration, err := rand.Int(rand.Reader, big.NewInt(int64(maxJitter)))
+ if err == nil {
+ jitter = time.Duration(randDuration.Int64())
+ }
}
}
diff --git a/server/service/embedded_scripts/delete-duplicate-scep-certificates.sh b/server/service/embedded_scripts/delete-duplicate-scep-certificates.sh
index 5df7ceeca84..51b8ecaa256 100755
--- a/server/service/embedded_scripts/delete-duplicate-scep-certificates.sh
+++ b/server/service/embedded_scripts/delete-duplicate-scep-certificates.sh
@@ -59,6 +59,14 @@ trap 'rm -f "$tmpfile" "$tmpfile.raw" "$tmpfile.err"' EXIT
security find-certificate -a -c "$CN" -Z -p "$KEYCHAIN" >"$tmpfile.raw" 2>"$tmpfile.err" || true
+begin_count=$(grep -c -- "-----BEGIN CERTIFICATE-----" "$tmpfile.raw" || true)
+end_count=$(grep -c -- "-----END CERTIFICATE-----" "$tmpfile.raw" || true)
+hash_count=$(grep -c "^SHA-1 hash:" "$tmpfile.raw" || true)
+if [ "$begin_count" -ne "$end_count" ] || [ "$begin_count" -ne "$hash_count" ]; then
+ echo "Error: unexpected output from 'security find-certificate' (found $hash_count hash(es), $begin_count BEGIN marker(s), $end_count END marker(s)); refusing to proceed to avoid deleting the wrong identity." >&2
+ exit 1
+fi
+
# Split the raw output into individual cert blocks and extract hash + date.
current_hash=""
current_pem=""
@@ -66,6 +74,10 @@ while IFS= read -r line; do
case "$line" in
"SHA-1 hash:"*)
current_hash=$(echo "$line" | awk '{print $NF}')
+ if ! echo "$current_hash" | grep -Eq '^[0-9A-Fa-f]{40}$'; then
+ echo "Error: malformed SHA-1 hash encountered (\"$current_hash\"); refusing to proceed." >&2
+ exit 1
+ fi
;;
"-----BEGIN CERTIFICATE-----")
current_pem="$line"$'\n'
@@ -73,7 +85,11 @@ while IFS= read -r line; do
"-----END CERTIFICATE-----")
current_pem+="$line"$'\n'
not_before=$(echo "$current_pem" | openssl x509 -noout -startdate 2>/dev/null | cut -d= -f2)
- epoch=$(date -j -f "%b %e %T %Y %Z" "$not_before" "+%s" 2>/dev/null || echo "0")
+ epoch=$(date -j -f "%b %e %T %Y %Z" "$not_before" "+%s" 2>/dev/null || echo "")
+ if [ -z "$epoch" ]; then
+ echo "Error: failed to parse Not Before date (\"$not_before\") for certificate $current_hash; refusing to proceed to avoid deleting the wrong identity." >&2
+ exit 1
+ fi
echo "$epoch $current_hash" >> "$tmpfile"
current_pem=""
;;
diff --git a/server/service/externalsvc/jira.go b/server/service/externalsvc/jira.go
index c142c22f37f..d076de03828 100644
--- a/server/service/externalsvc/jira.go
+++ b/server/service/externalsvc/jira.go
@@ -3,6 +3,7 @@ package externalsvc
import (
"context"
"errors"
+ "log"
"net"
"net/http"
"strconv"
@@ -118,6 +119,11 @@ func doWithRetry(fn func() (*jira.Response, error)) error {
}
}
+ if resp == nil {
+ // no response received (e.g. connection error); treat as retryable
+ return err
+ }
+
if resp.StatusCode >= http.StatusInternalServerError {
// 500+ status, can be worth retrying
return err
@@ -128,8 +134,11 @@ func doWithRetry(fn func() (*jira.Response, error)) error {
// https://developer.atlassian.com/cloud/jira/platform/rate-limiting/
// for details.
rawAfter := resp.Header.Get("Retry-After")
- afterSecs, err := strconv.ParseInt(rawAfter, 10, 0)
- if err == nil && (time.Duration(afterSecs)*time.Second) < maxWaitForRetryAfter {
+ afterSecs, parseErr := strconv.ParseInt(rawAfter, 10, 0)
+ if parseErr != nil {
+ log.Printf("jira: failed to parse Retry-After header %q: %v", rawAfter, parseErr)
+ }
+ if parseErr == nil && (time.Duration(afterSecs)*time.Second) < maxWaitForRetryAfter {
// the retry-after duration is reasonable, wait for it and return a
// retryable error so that we try again.
time.Sleep(time.Duration(afterSecs) * time.Second)
diff --git a/server/service/jitter_test.go b/server/service/jitter_test.go
index 768ac58a5cc..d18d09362f6 100644
--- a/server/service/jitter_test.go
+++ b/server/service/jitter_test.go
@@ -35,7 +35,7 @@ func TestJitterForHost(t *testing.T) {
t.Logf("min=%d \t max=%d \t variation=%d\n", minVal, maxVal, variation)
// check that variation is below 1% of the total amount of hosts
- require.Less(t, variation, int(float32(hostCount)/0.01))
+ require.Less(t, variation, int(float32(hostCount)*0.01))
}
func TestNoJitter(t *testing.T) {
diff --git a/server/service/software_installers.go b/server/service/software_installers.go
index f909631fc65..15002636a4a 100644
--- a/server/service/software_installers.go
+++ b/server/service/software_installers.go
@@ -55,6 +55,7 @@ type updateSoftwareInstallerRequest struct {
LabelsIncludeAll []string
Categories []string
DisplayName *string
+ AutomaticInstall *bool
// Configuration is the in-house app's managed app configuration as raw XML bytes (iOS / iPadOS only). nil means leave unchanged.
Configuration []byte
}
@@ -64,8 +65,13 @@ type uploadSoftwareInstallerResponse struct {
Err error `json:"error,omitempty"`
}
-// TODO: We parse the whole body before running svc.authz.Authorize.
-// An authenticated but unauthorized user could abuse this.
+// NOTE: This handler parses the whole multipart body (including a potentially
+// large file upload) before svc.authz.Authorize runs. An authenticated but
+// unauthorized user could abuse this to force the server to buffer/parse
+// arbitrary large uploads before being rejected. This is a known
+// resource-exhaustion risk that should be addressed by moving authorization
+// earlier in the request pipeline (e.g. checking authz before or during body
+// parsing) rather than after DecodeRequest completes.
func (updateSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
decoded := updateSoftwareInstallerRequest{}
@@ -153,6 +159,15 @@ func (updateSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http
decoded.SelfService = &parsed
}
+ val, ok = r.MultipartForm.Value["automatic_install"]
+ if ok && len(val) > 0 && val[0] != "" {
+ parsed, err := strconv.ParseBool(val[0])
+ if err != nil {
+ return nil, &fleet.BadRequestError{Message: fmt.Sprintf("failed to decode automatic_install bool in multipart form: %s", err.Error())}
+ }
+ decoded.AutomaticInstall = &parsed
+ }
+
// decode labels and categories
var inclAny, exclAny, inclAll, categories []string
var existsInclAny, existsExclAny, existsInclAll, existsCategories bool
@@ -293,8 +308,13 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
return nil, fleet.ErrMissingLicense
}
-// TODO: We parse the whole body before running svc.authz.Authorize.
-// An authenticated but unauthorized user could abuse this.
+// NOTE: This handler parses the whole multipart body (including a potentially
+// large file upload) before svc.authz.Authorize runs. An authenticated but
+// unauthorized user could abuse this to force the server to buffer/parse
+// arbitrary large uploads before being rejected. This is a known
+// resource-exhaustion risk that should be addressed by moving authorization
+// earlier in the request pipeline (e.g. checking authz before or during body
+// parsing) rather than after DecodeRequest completes.
func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error) {
decoded := uploadSoftwareInstallerRequest{}
@@ -1153,3 +1173,4 @@ func (svc *Service) GetInHouseAppPackage(ctx context.Context, titleID uint, toke
return nil, fleet.ErrMissingLicense
}
+
diff --git a/server/vulnerabilities/io/github.go b/server/vulnerabilities/io/github.go
index 0dea9be7096..b03544cd91e 100644
--- a/server/vulnerabilities/io/github.go
+++ b/server/vulnerabilities/io/github.go
@@ -110,7 +110,7 @@ func (gh GitHubClient) MacOfficeReleaseNotes(ctx context.Context) (MetadataFileN
}
// Nothing found ...
- return MetadataFileName{}, "", nil
+ return MetadataFileName{}, "", errors.New("no MacOffice release notes found")
}
// list iterates over the latest release in our Github NVD repo
@@ -128,7 +128,7 @@ func (gh GitHubClient) list(ctx context.Context, prefix string, ctor func(fileNa
&github.ListOptions{Page: 0, PerPage: 10},
)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("list github releases for prefix %q: %w", prefix, err)
}
if r.StatusCode != http.StatusOK {
@@ -143,7 +143,7 @@ func (gh GitHubClient) list(ctx context.Context, prefix string, ctor func(fileNa
if strings.HasPrefix(name, prefix) {
metadataFileName, err := ctor(name)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("build metadata file name for asset %q (prefix %q): %w", name, prefix, err)
}
results[metadataFileName] = e.GetBrowserDownloadURL()
}
diff --git a/server/vulnerabilities/osv/sync.go b/server/vulnerabilities/osv/sync.go
index f9608f59220..ef3f7529912 100644
--- a/server/vulnerabilities/osv/sync.go
+++ b/server/vulnerabilities/osv/sync.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/rs/zerolog/log"
)
const (
@@ -43,9 +44,8 @@ func Refresh(
upToDateVersions := make([]string, 0, len(syncResult.Downloaded)+len(syncResult.Skipped))
upToDateVersions = append(upToDateVersions, syncResult.Downloaded...)
upToDateVersions = append(upToDateVersions, syncResult.Skipped...)
- err = removeOldOSVArtifacts(now, vulnPath, upToDateVersions)
- if err != nil {
- return syncResult.Downloaded, fmt.Errorf("warning: failed to clean up old OSV artifacts: %w", err)
+ if err := removeOldOSVArtifacts(now, vulnPath, upToDateVersions); err != nil {
+ log.Warn().Err(err).Msg("failed to clean up old OSV artifacts")
}
return syncResult.Downloaded, nil
@@ -270,7 +270,7 @@ func RefreshRHEL(
upToDateVersions = append(upToDateVersions, syncResult.Downloaded...)
upToDateVersions = append(upToDateVersions, syncResult.Skipped...)
if err := removeOldRHELOSVArtifacts(now, vulnPath, upToDateVersions); err != nil {
- return syncResult.Downloaded, fmt.Errorf("warning: failed to clean up old RHEL OSV artifacts: %w", err)
+ log.Warn().Err(err).Msg("failed to clean up old RHEL OSV artifacts")
}
return syncResult.Downloaded, nil
diff --git a/tools/github-manage/pkg/ghapi/issues.go b/tools/github-manage/pkg/ghapi/issues.go
index 32e00e31791..584603c8662 100644
--- a/tools/github-manage/pkg/ghapi/issues.go
+++ b/tools/github-manage/pkg/ghapi/issues.go
@@ -127,14 +127,14 @@ func RemoveIssueFromProject(issueNumber int, projectID int) error {
if err.Error() == fmt.Sprintf("issue #%d not found in project %d", issueNumber, projectID) {
return nil
}
- return fmt.Errorf("failed to get project item ID: %v", err)
+ return fmt.Errorf("failed to get project item ID: %w", err)
}
// Remove the item using the project ID and item ID
command := fmt.Sprintf("gh project item-delete %d --owner fleetdm --id %s", projectID, itemID)
_, err = RunCommandAndReturnOutput(command)
if err != nil {
- return fmt.Errorf("failed to remove issue %d from project %d: %v", issueNumber, projectID, err)
+ return fmt.Errorf("failed to remove issue %d from project %d: %w", issueNumber, projectID, err)
}
return nil
@@ -145,19 +145,19 @@ func SyncEstimateField(issueNumber int, sourceProjectID, targetProjectID int) er
// Get the source project item to find the current estimate
sourceItemID, err := GetProjectItemID(issueNumber, sourceProjectID)
if err != nil {
- return fmt.Errorf("failed to get source project item ID: %v", err)
+ return fmt.Errorf("failed to get source project item ID: %w", err)
}
// Get the target project item
targetItemID, err := GetProjectItemID(issueNumber, targetProjectID)
if err != nil {
- return fmt.Errorf("failed to get target project item ID: %v", err)
+ return fmt.Errorf("failed to get target project item ID: %w", err)
}
// Get the estimate value from the source project using GraphQL
sourceEstimate, err := GetProjectItemFieldValue(sourceItemID, sourceProjectID, "Estimate")
if err != nil {
- return fmt.Errorf("failed to get source estimate: %v", err)
+ return fmt.Errorf("failed to get source estimate: %w", err)
}
if sourceEstimate == "" || sourceEstimate == "0" {
@@ -167,7 +167,7 @@ func SyncEstimateField(issueNumber int, sourceProjectID, targetProjectID int) er
// Set the estimate in the target project
err = SetProjectItemFieldValue(targetItemID, targetProjectID, "Estimate", sourceEstimate)
if err != nil {
- return fmt.Errorf("failed to set target estimate: %v", err)
+ return fmt.Errorf("failed to set target estimate: %w", err)
}
return nil
@@ -178,19 +178,19 @@ func SetCurrentSprint(issueNumber int, projectID int) error {
// Get the project item ID
itemID, err := GetProjectItemID(issueNumber, projectID)
if err != nil {
- return fmt.Errorf("failed to get project item ID: %v", err)
+ return fmt.Errorf("failed to get project item ID: %w", err)
}
// Look up the sprint field ID
sprintField, err := LookupProjectFieldName(projectID, "sprint")
if err != nil {
- return fmt.Errorf("failed to lookup sprint field: %v", err)
+ return fmt.Errorf("failed to lookup sprint field: %w", err)
}
// Use the general field setting function to set the sprint field to @current
err = SetProjectItemFieldValue(itemID, projectID, sprintField.Name, "@current")
if err != nil {
- return fmt.Errorf("failed to set current sprint: %v", err)
+ return fmt.Errorf("failed to set current sprint: %w", err)
}
return nil
@@ -201,13 +201,13 @@ func SetIssueStatus(issueNumber int, projectID int, status string) error {
// Get the project item ID
itemID, err := GetProjectItemID(issueNumber, projectID)
if err != nil {
- return fmt.Errorf("failed to get project item ID: %v", err)
+ return fmt.Errorf("failed to get project item ID: %w", err)
}
// Use the general field setting function to set the Status field
err = SetProjectItemFieldValue(itemID, projectID, "Status", status)
if err != nil {
- return fmt.Errorf("failed to set status: %v", err)
+ return fmt.Errorf("failed to set status: %w", err)
}
return nil
@@ -371,7 +371,10 @@ listLoop:
for i, issue := range issues {
// if we find an error, we'll drain concurrent executions and then bail
- if stopError != nil {
+ mu.Lock()
+ stopped := stopError != nil
+ mu.Unlock()
+ if stopped {
break
}
@@ -400,7 +403,9 @@ listLoop:
fmt.Fprintf(os.Stderr, " ERROR\n")
mu.Unlock()
}
+ mu.Lock()
stopError = err
+ mu.Unlock()
logger.Errorf("Error checking timeline for issue #%d: %v", iss.Number, err)
return
}
@@ -423,5 +428,9 @@ listLoop:
fmt.Fprintf(os.Stderr, " done\n")
}
- return filteredIssues, stopError
+ mu.Lock()
+ finalErr := stopError
+ mu.Unlock()
+
+ return filteredIssues, finalErr
}
diff --git a/tools/gitops-migrate/migrate.sh b/tools/gitops-migrate/migrate.sh
index 6ee60794caa..9aa6c97f765 100755
--- a/tools/gitops-migrate/migrate.sh
+++ b/tools/gitops-migrate/migrate.sh
@@ -74,7 +74,6 @@ validate_yaml() {
extract_keys_from_software() {
local software_file="$1"
local temp_file=$(mktemp -p .)
- chmod 666 $temp_file
# Extract the keys we need
{
@@ -93,7 +92,6 @@ remove_keys_from_software() {
# Create a temporary file with keys removed
local temp_file=$(mktemp -p .)
- chmod 666 $temp_file
yq eval --output-format=yaml 'del(.self_service, .categories, .labels_include_any, .labels_exclude_any)' "$software_file" > "$temp_file"
# Replace the original file
@@ -190,7 +188,7 @@ process_team_file() {
# Clean up temp file
rm -f "$keys_temp_file"
- PROCESSED_PACKAGED=$((PROCESSED_PACKAGES+1))
+ PROCESSED_PACKAGES=$((PROCESSED_PACKAGES+1))
echo -e "${GREEN} ✓ Package processed successfully${NC}"
done
@@ -333,3 +331,4 @@ main() {
# Run main function with all script arguments
main "$@"
+
diff --git a/tools/mdm/migration/mdmproxy/entrypoint.sh b/tools/mdm/migration/mdmproxy/entrypoint.sh
index 8177f1c912d..347f53eb7f9 100644
--- a/tools/mdm/migration/mdmproxy/entrypoint.sh
+++ b/tools/mdm/migration/mdmproxy/entrypoint.sh
@@ -1,31 +1,27 @@
#!/bin/sh
set -e
-AUTH_TOKEN_ARG=""
-MIGRATE_PERCENTAGE_ARG=""
-MIGRATE_UDIDS_ARG=""
if [ -z "${MDMPROXY_SERVER_ADDRESS}" ]; then
MDMPROXY_SERVER_ADDRESS=":8080"
fi
+set -- \
+ -existing-hostname "${MDMPROXY_EXISTING_HOSTNAME:?}" \
+ -existing-url "${MDMPROXY_EXISTING_URL:?}" \
+ -fleet-url "${MDMPROXY_FLEET_URL:?}" \
+ -server-address "${MDMPROXY_SERVER_ADDRESS:?}"
+
if [ -n "${MDMPROXY_AUTH_TOKEN}" ]; then
- AUTH_TOKEN_ARG="-auth-token \"${MDMPROXY_AUTH_TOKEN:?}\""
+ set -- "$@" -auth-token "${MDMPROXY_AUTH_TOKEN:?}"
fi
if [ -n "${MDMPROXY_MIGRATE_PERCENTAGE}" ]; then
- MIGRATE_PERCENTAGE_ARG="-migrate-percentage \"${MDMPROXY_MIGRATE_PERCENTAGE:?}\""
+ set -- "$@" -migrate-percentage "${MDMPROXY_MIGRATE_PERCENTAGE:?}"
fi
if [ -n "${MDMPROXY_MIGRATE_UDIDS}" ]; then
- MIGRATE_UDIDS_ARG="-migrate-udids \"${MDMPROXY_MIGRATE_UDIDS:?}\""
+ set -- "$@" -migrate-udids "${MDMPROXY_MIGRATE_UDIDS:?}"
fi
-eval exec /usr/bin/mdmproxy \
- ${AUTH_TOKEN_ARG} \
- -existing-hostname "${MDMPROXY_EXISTING_HOSTNAME:?}" \
- -existing-url "${MDMPROXY_EXISTING_URL:?}" \
- -fleet-url "${MDMPROXY_FLEET_URL:?}" \
- ${MIGRATE_PERCENTAGE_ARG} \
- ${MIGRATE_UDIDS_ARG} \
- -server-address "${MDMPROXY_SERVER_ADDRESS:?}"
+exec /usr/bin/mdmproxy "$@"
diff --git a/tools/mdm/migration/simplemdm/main.go b/tools/mdm/migration/simplemdm/main.go
index 1c4231101a3..f73f808c1cd 100644
--- a/tools/mdm/migration/simplemdm/main.go
+++ b/tools/mdm/migration/simplemdm/main.go
@@ -18,7 +18,6 @@ import (
const DELAY = 10 * time.Second // adjust this to simulate slow webhook response
var (
- apiTokenFlag = flag.String("api-token", "", "API token")
deviceIDFlag = flag.String("device-id", "", "Device ID to unenroll")
)
@@ -48,7 +47,7 @@ func newSimpleClient(apiToken string) *simpleClient {
// if err != nil {
// return 0, err
// }
-// req.SetBasicAuth(*apiTokenFlag, "")
+// req.SetBasicAuth(c.apiToken, "")
// req.Header.Set("Content-Type", "application/json")
// resp, err := client.Do(req)
// if err != nil {
@@ -75,7 +74,7 @@ func (c *simpleClient) unenroll(deviceID uint) error {
if err != nil {
return err
}
- req.SetBasicAuth(*apiTokenFlag, "")
+ req.SetBasicAuth(c.apiToken, "")
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
@@ -98,8 +97,9 @@ func (c *simpleClient) unenroll(deviceID uint) error {
func main() {
flag.Parse()
- if *apiTokenFlag == "" {
- log.Fatal("--api-token must be provided")
+ apiToken := os.Getenv("SIMPLEMDM_API_TOKEN")
+ if apiToken == "" {
+ log.Fatal("SIMPLEMDM_API_TOKEN environment variable must be set")
}
if *deviceIDFlag == "" {
@@ -110,6 +110,8 @@ func main() {
log.Fatalf("invalid device ID %s: %v", *deviceIDFlag, err)
}
+ client := newSimpleClient(apiToken)
+
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
var detail string
body, err := io.ReadAll(request.Body)
@@ -132,11 +134,15 @@ func main() {
// }
// TODO: Use getDeviceIDBySerial to find the device ID by serial number
- // For now, we just use the device ID provided via command line flag.
+ // from the parsed request body above, instead of relying on the
+ // device ID provided via command line flag. Until that is
+ // implemented, this handler will always unenroll the device
+ // identified by --device-id, regardless of which host triggered
+ // the webhook.
time.Sleep(DELAY)
- if err := newSimpleClient(*apiTokenFlag).unenroll(uint(deviceID)); err != nil {
+ if err := client.unenroll(uint(deviceID)); err != nil {
log.Printf("error unenrolling device %d: %s", deviceID, err.Error())
writer.WriteHeader(http.StatusBadGateway)
if _, err := writer.Write([]byte("Error unenrolling device")); err != nil {
diff --git a/tools/team-builder/build_teams.sh b/tools/team-builder/build_teams.sh
index 68766a0264b..6c4c5aefd54 100755
--- a/tools/team-builder/build_teams.sh
+++ b/tools/team-builder/build_teams.sh
@@ -12,7 +12,7 @@ run(){
while getopts s:p:u:f:d:o:x flag
do
case "${flag}" in
- f) #path to file containing team names. Must end with newline char.
+ s) #path to file containing team names. Must end with newline char.
source=($OPTARG);;
p) #types of installers to create. Pass an individual flag for each type
types+=($OPTARG);;
diff --git a/tools/terraform/provider/teams_resource.go b/tools/terraform/provider/teams_resource.go
index 48731a4fa73..872f6d84f7b 100644
--- a/tools/terraform/provider/teams_resource.go
+++ b/tools/terraform/provider/teams_resource.go
@@ -131,7 +131,13 @@ func (r *teamsResource) Create(ctx context.Context, req resource.CreateRequest,
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
"failed to convert fleet api return to TF structs",
fmt.Sprintf("failed to convert fleet api return to TF structs: %s", err)))
- _ = r.client.DeleteTeam(newTeam.Team.ID) // Problematic. :-/
+ if delErr := r.client.DeleteTeam(newTeam.Team.ID); delErr != nil {
+ resp.Diagnostics.Append(diag.NewErrorDiagnostic(
+ "failed to clean up after failed team conversion",
+ fmt.Sprintf("failed to delete team %s while cleaning up "+
+ "failure converting API response: %s. Team will need to be "+
+ "manually deleted.", plan.Name.ValueString(), delErr)))
+ }
return
}
@@ -247,6 +253,19 @@ func (r *teamsResource) Update(ctx context.Context, req resource.UpdateRequest,
}
}
+ if upTeam == nil {
+ // Nothing was actually updated on the Fleet side (e.g. agent options
+ // changed to an empty string and name/description are unchanged), so
+ // fetch the current team to safely populate state.
+ upTeam, err = r.client.GetTeam(state.Id.ValueInt64())
+ if err != nil {
+ resp.Diagnostics.Append(diag.NewErrorDiagnostic(
+ "Failed to get team",
+ fmt.Sprintf("Failed to get team: %s", err)))
+ return
+ }
+ }
+
err = teamModelToTF(ctx, upTeam, &state)
if err != nil {
resp.Diagnostics.Append(diag.NewErrorDiagnostic(
@@ -297,3 +316,4 @@ func (r *teamsResource) Delete(ctx context.Context, req resource.DeleteRequest,
resp.State.RemoveResource(ctx)
}
+
diff --git a/website/api/controllers/android-proxy/get-android-enterprises.js b/website/api/controllers/android-proxy/get-android-enterprises.js
index 69876343b56..e17d3fc7e07 100644
--- a/website/api/controllers/android-proxy/get-android-enterprises.js
+++ b/website/api/controllers/android-proxy/get-android-enterprises.js
@@ -18,6 +18,7 @@ module.exports = {
missingOriginHeader: { description: 'The request was missing an Origin header', responseType: 'badRequest'},
unauthorized: { description: 'Invalid authentication token.', responseType: 'unauthorized'},
notFound: { description: 'No Android enterprise found for this Fleet server.', responseType: 'notFound'},
+ rateLimited: { description: 'The Android management API rate limit was exceeded.', responseType: 'tooManyRequests' },
},
@@ -46,11 +47,18 @@ module.exports = {
throw 'notFound';
}
- if (thisAndroidEnterprise.fleetServerSecret !== fleetServerSecret) {
+ let crypto = require('crypto');
+ let providedSecretBuffer = Buffer.from(fleetServerSecret);
+ let storedSecretBuffer = Buffer.from(thisAndroidEnterprise.fleetServerSecret);
+ let secretsMatch = providedSecretBuffer.length === storedSecretBuffer.length &&
+ crypto.timingSafeEqual(providedSecretBuffer, storedSecretBuffer);
+
+ if (!secretsMatch) {
throw 'unauthorized';
}
// Get the Android enterprises list from Google
+ let isRateLimited = false;
try {
let enterprisesList = await sails.helpers.flow.build(async ()=>{
let { google } = require('googleapis');
@@ -93,6 +101,7 @@ module.exports = {
}).intercept({status: 429}, (err)=>{
// If the Android management API returns a 429 response, log an additional warning that will trigger a help-p1 alert.
sails.log.warn(`p1: Android management API rate limit exceeded!`);
+ isRateLimited = true;
return err;
}).intercept((err)=>{
// Re-throw the error for handling outside the intercept
@@ -114,6 +123,9 @@ module.exports = {
return { enterprises: filteredEnterprises };
} catch (err) {
+ if (isRateLimited) {
+ throw 'rateLimited';
+ }
throw new Error(`When attempting to list android enterprises, an error occurred. Error: ${err}`);
}
diff --git a/website/api/controllers/articles/view-basic-webinar.js b/website/api/controllers/articles/view-basic-webinar.js
index fa3cfc2288e..43aaefd897b 100644
--- a/website/api/controllers/articles/view-basic-webinar.js
+++ b/website/api/controllers/articles/view-basic-webinar.js
@@ -44,11 +44,11 @@ module.exports = {
let pageTitleForMeta;
let pageDescriptionForMeta;
- if(thisPage.meta.articleTitle) {
+ if(thisPage.meta && thisPage.meta.articleTitle) {
pageTitleForMeta = thisPage.meta.articleTitle;
}
- if(thisPage.meta.description) {
+ if(thisPage.meta && thisPage.meta.description) {
pageDescriptionForMeta = thisPage.meta.description;
}
diff --git a/website/api/helpers/microsoft-proxy/get-access-token-and-api-urls.js b/website/api/helpers/microsoft-proxy/get-access-token-and-api-urls.js
index 17e3018a2f6..c6b189ca6e4 100644
--- a/website/api/helpers/microsoft-proxy/get-access-token-and-api-urls.js
+++ b/website/api/helpers/microsoft-proxy/get-access-token-and-api-urls.js
@@ -28,7 +28,7 @@ module.exports = {
let informationAboutThisTenant = await MicrosoftComplianceTenant.findOne({id: complianceTenantRecordId});
if(!informationAboutThisTenant) {
- return new Error(`No matching tenant record could be found with the specified ID. (${complianceTenantRecordId}`);
+ throw new Error(`No matching tenant record could be found with the specified ID. (${complianceTenantRecordId})`);
}
// Get a graph access token for this tenant
@@ -109,3 +109,4 @@ module.exports = {
};
+
diff --git a/website/assets/js/pages/docs/vital-details.page.js b/website/assets/js/pages/docs/vital-details.page.js
index 20f1a74ccb7..7d2d853626c 100644
--- a/website/assets/js/pages/docs/vital-details.page.js
+++ b/website/assets/js/pages/docs/vital-details.page.js
@@ -85,11 +85,13 @@ parasails.registerPage('vital-details', {
})();
$('[purpose="copy-button"]').on('click', async function() {
let code = $(this).closest('[purpose="codeblock"]').find('pre:visible code').text();
- $(this).addClass('copied');
- await setTimeout(()=>{
- $(this).removeClass('copied');
- }, 2000);
- navigator.clipboard.writeText(code);
+ if(code) {
+ $(this).addClass('copied');
+ await setTimeout(()=>{
+ $(this).removeClass('copied');
+ }, 2000);
+ navigator.clipboard.writeText(code);
+ }
});
// Add a scroll event listener to shift the platform filters upwards when the header is hidden.
window.addEventListener('scroll', this.handleScrollingPlatformFilters);
@@ -148,3 +150,4 @@ parasails.registerPage('vital-details', {
}
},
});
+
diff --git a/website/assets/resources/install-fleetctl.sh b/website/assets/resources/install-fleetctl.sh
index 9d0aad5fa15..01f118b30fe 100644
--- a/website/assets/resources/install-fleetctl.sh
+++ b/website/assets/resources/install-fleetctl.sh
@@ -6,7 +6,7 @@ FLEETCTL_INSTALL_DIR="${HOME}/.fleetctl/"
# Check for necessary commands
-for cmd in curl tar grep sed; do
+for cmd in curl tar grep sed shasum; do
if ! command -v $cmd &> /dev/null; then
echo "Error: $cmd is not installed." >&2
exit 1
@@ -20,6 +20,13 @@ echo "Fetching the latest version of fleetctl..."
latest_strippedVersion=$(curl -s "https://registry.npmjs.org/fleetctl/latest" | grep -o '"version": *"[^"]*"' | cut -d'"' -f4)
echo "Latest version available on NPM: $latest_strippedVersion"
+# Validate that the version string looks like a semver value before using it
+# in a URL or filesystem path.
+if ! [[ "$latest_strippedVersion" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "Error: Unexpected version string received from NPM: '${latest_strippedVersion}'" >&2
+ exit 1
+fi
+
version_gt() {
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1";
}
@@ -48,10 +55,36 @@ mkdir -p "${FLEETCTL_INSTALL_DIR}"
# Construct download URL
# ex: https://github.com/fleetdm/fleet/releases/download/fleet-v4.43.3/fleetctl_v4.43.3_macos.zip
DOWNLOAD_URL="https://github.com/fleetdm/fleet/releases/download/fleet-v${latest_strippedVersion}/fleetctl_v${latest_strippedVersion}_${OS}.tar.gz"
+CHECKSUMS_URL="https://github.com/fleetdm/fleet/releases/download/fleet-v${latest_strippedVersion}/fleetctl_v${latest_strippedVersion}.checksums.txt"
-# Download the latest version of fleetctl and extract it.
+# Download the latest version of fleetctl, verify its checksum, then extract it.
echo "Downloading fleetctl ${latest_strippedVersion} for ${OS_DISPLAY_NAME}..."
-curl -sSL "$DOWNLOAD_URL" | tar -xz -C "$FLEETCTL_INSTALL_DIR" --strip-components=1 fleetctl_v"${latest_strippedVersion}"_${OS}/
+
+TMP_DIR=$(mktemp -d)
+trap 'rm -rf "${TMP_DIR}"' EXIT
+
+ARCHIVE_NAME="fleetctl_v${latest_strippedVersion}_${OS}.tar.gz"
+ARCHIVE_PATH="${TMP_DIR}/${ARCHIVE_NAME}"
+CHECKSUMS_PATH="${TMP_DIR}/fleetctl_v${latest_strippedVersion}.checksums.txt"
+
+curl -sSL -o "${ARCHIVE_PATH}" "$DOWNLOAD_URL"
+curl -sSL -o "${CHECKSUMS_PATH}" "$CHECKSUMS_URL"
+
+EXPECTED_SHA=$(grep " ${ARCHIVE_NAME}\$" "${CHECKSUMS_PATH}" | awk '{print $1}')
+if [[ -z "$EXPECTED_SHA" ]]; then
+ echo "Error: Could not find checksum for ${ARCHIVE_NAME} in checksums file." >&2
+ exit 1
+fi
+
+ACTUAL_SHA=$(shasum -a 256 "${ARCHIVE_PATH}" | awk '{print $1}')
+if [[ "$EXPECTED_SHA" != "$ACTUAL_SHA" ]]; then
+ echo "Error: Checksum verification failed for ${ARCHIVE_NAME}." >&2
+ echo "Expected: ${EXPECTED_SHA}" >&2
+ echo "Actual: ${ACTUAL_SHA}" >&2
+ exit 1
+fi
+
+tar -xz -f "${ARCHIVE_PATH}" -C "$FLEETCTL_INSTALL_DIR" --strip-components=1 fleetctl_v"${latest_strippedVersion}"_${OS}/
echo "fleetctl installed successfully in ${FLEETCTL_INSTALL_DIR}"
# Verify if the binary is executable
@@ -59,3 +92,4 @@ if [[ ! -x "${FLEETCTL_INSTALL_DIR}/fleetctl" ]]; then
echo "Failed to install or upgrade fleetctl. Please check your permissions and try running this script again."
exit 1
fi
+