This commit is contained in:
2026-07-25 15:57:12 -05:00
parent 90d5b637ac
commit b20f14e32d
7 changed files with 60 additions and 26 deletions
+7 -5
View File
@@ -122,10 +122,12 @@ For later updates, run the repository update helper on one webserver at a time:
/opt/idssys/ta-deployment-broker/update.sh /opt/idssys/ta-deployment-broker/update.sh
``` ```
The helper fast-forwards the current tracked branch, rebuilds the image, applies The helper fetches and compares the current tracked branch with its upstream. If
any pending migrations, recreates the broker container, and waits up to 150 both commits match, it exits without touching Docker. When an update exists, it
seconds for its Docker health check. It refuses to pull over tracked local fast-forwards the branch, rebuilds the image, applies any pending migrations,
changes or from a detached/untracked branch. recreates the broker container, and waits up to 150 seconds for its Docker
health check. It refuses to pull over tracked local changes or from a detached,
untracked, locally-ahead, or diverged branch.
## 7. HAProxy health check ## 7. HAProxy health check
@@ -178,7 +180,7 @@ technician's Gitea credentials for package operations.
- Audit records are retained indefinitely unless an administrator establishes a - Audit records are retained indefinitely unless an administrator establishes a
database retention policy. The Codes view previews the newest 15 records; the database retention policy. The Codes view previews the newest 15 records; the
Audit view can filter retained history by event, customer/deployment label, Audit view can filter retained history by event, customer/deployment label,
actor, host, package, source IP, or details and displays up to the newest 250 user, host, package, source IP, or details and displays up to the newest 250
matching events. matching events.
- Keep `.env` outside Git and readable only by the service administrator. - Keep `.env` outside Git and readable only by the service administrator.
- Rotate both Gitea package tokens and the OAuth secret if either webserver is - Rotate both Gitea package tokens and the OAuth secret if either webserver is
+12 -4
View File
@@ -11,7 +11,7 @@ func TestAuditFiltersFromRequest(t *testing.T) {
t.Parallel() t.Parallel()
request := httptest.NewRequest( request := httptest.NewRequest(
"GET", "GET",
"/portal?audit_range=7d&audit_event=package_downloaded&audit_customer=Acme&audit_actor=taiadmin&audit_hostname=pve01&audit_package=sentinelone-linux&audit_ip=10.10.1.25&audit_details=install-rmm", "/portal?audit_range=7d&audit_event=package_downloaded&audit_customer=Acme&audit_user=taiadmin&audit_hostname=pve01&audit_package=sentinelone-linux&audit_ip=10.10.1.25&audit_details=install-rmm",
nil, nil,
) )
filters := auditFiltersFromRequest(request) filters := auditFiltersFromRequest(request)
@@ -19,7 +19,7 @@ func TestAuditFiltersFromRequest(t *testing.T) {
TimeRange: "7d", TimeRange: "7d",
EventType: "package_downloaded", EventType: "package_downloaded",
CustomerLabel: "Acme", CustomerLabel: "Acme",
Actor: "taiadmin", User: "taiadmin",
Hostname: "pve01", Hostname: "pve01",
PackageSlug: "sentinelone-linux", PackageSlug: "sentinelone-linux",
SourceIP: "10.10.1.25", SourceIP: "10.10.1.25",
@@ -38,6 +38,14 @@ func TestAuditFiltersDefaultToThirtyDays(t *testing.T) {
} }
} }
func TestAuditFiltersAcceptLegacyActorParameter(t *testing.T) {
t.Parallel()
request := httptest.NewRequest("GET", "/portal?audit_actor=legacy-user", nil)
if filters := auditFiltersFromRequest(request); filters.User != "legacy-user" {
t.Fatalf("user = %q, want legacy-user", filters.User)
}
}
func TestHasAuditQuery(t *testing.T) { func TestHasAuditQuery(t *testing.T) {
t.Parallel() t.Parallel()
if !hasAuditQuery(httptest.NewRequest("GET", "/portal?audit_hostname=pve01", nil)) { if !hasAuditQuery(httptest.NewRequest("GET", "/portal?audit_hostname=pve01", nil)) {
@@ -54,7 +62,7 @@ func TestAuditQueryUsesPlaceholders(t *testing.T) {
TimeRange: "all", TimeRange: "all",
EventType: "package_downloaded", EventType: "package_downloaded",
CustomerLabel: "Acme", CustomerLabel: "Acme",
Actor: "taiadmin", User: "taiadmin",
Hostname: "pve01", Hostname: "pve01",
PackageSlug: "sentinelone-linux", PackageSlug: "sentinelone-linux",
SourceIP: "10.10.1.25", SourceIP: "10.10.1.25",
@@ -62,7 +70,7 @@ func TestAuditQueryUsesPlaceholders(t *testing.T) {
} }
query, arguments := auditQuery(filters, 250) query, arguments := auditQuery(filters, 250)
if strings.Contains(query, filters.CustomerLabel) || if strings.Contains(query, filters.CustomerLabel) ||
strings.Contains(query, filters.Actor) || strings.Contains(query, filters.User) ||
strings.Contains(query, filters.Hostname) { strings.Contains(query, filters.Hostname) {
t.Fatal("filter values must not be interpolated into the SQL query") t.Fatal("filter values must not be interpolated into the SQL query")
} }
+11 -5
View File
@@ -62,6 +62,7 @@ func hasAuditQuery(r *http.Request) bool {
"audit_range", "audit_range",
"audit_event", "audit_event",
"audit_customer", "audit_customer",
"audit_user",
"audit_actor", "audit_actor",
"audit_hostname", "audit_hostname",
"audit_package", "audit_package",
@@ -155,11 +156,16 @@ func auditFiltersFromRequest(r *http.Request) auditFilters {
default: default:
timeRange = "30d" timeRange = "30d"
} }
userFilter := query.Get("audit_user")
if strings.TrimSpace(userFilter) == "" {
// Preserve audit links created before the portal used "user" terminology.
userFilter = query.Get("audit_actor")
}
return auditFilters{ return auditFilters{
TimeRange: timeRange, TimeRange: timeRange,
EventType: limitedFilter(query.Get("audit_event"), 100), EventType: limitedFilter(query.Get("audit_event"), 100),
CustomerLabel: limitedFilter(query.Get("audit_customer"), 255), CustomerLabel: limitedFilter(query.Get("audit_customer"), 255),
Actor: limitedFilter(query.Get("audit_actor"), 255), User: limitedFilter(userFilter, 255),
Hostname: limitedFilter(query.Get("audit_hostname"), 255), Hostname: limitedFilter(query.Get("audit_hostname"), 255),
PackageSlug: limitedFilter(query.Get("audit_package"), 100), PackageSlug: limitedFilter(query.Get("audit_package"), 100),
SourceIP: limitedFilter(query.Get("audit_ip"), 64), SourceIP: limitedFilter(query.Get("audit_ip"), 64),
@@ -224,7 +230,7 @@ func auditQuery(filters auditFilters, limit int) (string, []any) {
value string value string
}{ }{
{"COALESCE(a.customer_label, '')", filters.CustomerLabel}, {"COALESCE(a.customer_label, '')", filters.CustomerLabel},
{"ae.actor", filters.Actor}, {"ae.actor", filters.User},
{"ae.hostname", filters.Hostname}, {"ae.hostname", filters.Hostname},
{"ae.package_slug", filters.PackageSlug}, {"ae.package_slug", filters.PackageSlug},
{"ae.source_ip", filters.SourceIP}, {"ae.source_ip", filters.SourceIP},
@@ -260,7 +266,7 @@ func (s *Server) listAuditEvents(r *http.Request, filters auditFilters, limit in
if err := rows.Scan( if err := rows.Scan(
&record.EventType, &record.EventType,
&record.CustomerLabel, &record.CustomerLabel,
&record.Actor, &record.User,
&record.Hostname, &record.Hostname,
&record.PackageSlug, &record.PackageSlug,
&record.SourceIP, &record.SourceIP,
@@ -606,7 +612,7 @@ func validSHA256(value string) bool {
func (s *Server) audit( func (s *Server) audit(
ctx context.Context, ctx context.Context,
eventType string, eventType string,
actor string, user string,
authorizationID *uint64, authorizationID *uint64,
hostname string, hostname string,
packageSlug string, packageSlug string,
@@ -622,7 +628,7 @@ func (s *Server) audit(
`INSERT INTO audit_events `INSERT INTO audit_events
(event_type, actor, authorization_id, hostname, package_slug, source_ip, details) (event_type, actor, authorization_id, hostname, package_slug, source_ip, details)
VALUES (?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?)`,
eventType, actor, authID, hostname, packageSlug, sourceIP, details, eventType, user, authID, hostname, packageSlug, sourceIP, details,
) )
return err return err
} }
+2 -2
View File
@@ -71,7 +71,7 @@ type authorizationRecord struct {
type auditRecord struct { type auditRecord struct {
EventType string EventType string
CustomerLabel string CustomerLabel string
Actor string User string
Hostname string Hostname string
PackageSlug string PackageSlug string
SourceIP string SourceIP string
@@ -83,7 +83,7 @@ type auditFilters struct {
TimeRange string TimeRange string
EventType string EventType string
CustomerLabel string CustomerLabel string
Actor string User string
Hostname string Hostname string
PackageSlug string PackageSlug string
SourceIP string SourceIP string
+1 -1
View File
@@ -39,7 +39,7 @@
</select> </select>
</label> </label>
<label>Customer / deployment <input name="audit_customer" value="{{.AuditFilters.CustomerLabel}}" placeholder="Acme cluster refresh"></label> <label>Customer / deployment <input name="audit_customer" value="{{.AuditFilters.CustomerLabel}}" placeholder="Acme cluster refresh"></label>
<label>Technician / actor <input name="audit_actor" value="{{.AuditFilters.Actor}}" placeholder="taiadmin"></label> <label>User <input name="audit_user" value="{{.AuditFilters.User}}" placeholder="taiadmin"></label>
<label>Hostname <input name="audit_hostname" value="{{.AuditFilters.Hostname}}" placeholder="pve01"></label> <label>Hostname <input name="audit_hostname" value="{{.AuditFilters.Hostname}}" placeholder="pve01"></label>
<label>Package ID <input name="audit_package" value="{{.AuditFilters.PackageSlug}}" placeholder="sentinelone-linux"></label> <label>Package ID <input name="audit_package" value="{{.AuditFilters.PackageSlug}}" placeholder="sentinelone-linux"></label>
<label>Source IP <input name="audit_ip" value="{{.AuditFilters.SourceIP}}" placeholder="10.10.1.25"></label> <label>Source IP <input name="audit_ip" value="{{.AuditFilters.SourceIP}}" placeholder="10.10.1.25"></label>
+2 -2
View File
@@ -28,7 +28,7 @@
<div class="audit-row audit-header" aria-hidden="true"> <div class="audit-row audit-header" aria-hidden="true">
<span>Event</span> <span>Event</span>
<span>Customer / deployment</span> <span>Customer / deployment</span>
<span>Actor / host / package</span> <span>User / host / package</span>
<span>Source / details</span> <span>Source / details</span>
</div> </div>
{{range .AuditEvents}} {{range .AuditEvents}}
@@ -41,7 +41,7 @@
{{if .CustomerLabel}}<span>{{.CustomerLabel}}</span>{{else}}<span></span>{{end}} {{if .CustomerLabel}}<span>{{.CustomerLabel}}</span>{{else}}<span></span>{{end}}
</div> </div>
<div> <div>
{{if .Actor}}<span>{{.Actor}}</span>{{end}} {{if .User}}<span>{{.User}}</span>{{end}}
{{if .Hostname}}<span>{{.Hostname}}</span>{{end}} {{if .Hostname}}<span>{{.Hostname}}</span>{{end}}
{{if .PackageSlug}}<span>{{.PackageSlug}}</span>{{end}} {{if .PackageSlug}}<span>{{.PackageSlug}}</span>{{end}}
</div> </div>
+25 -7
View File
@@ -15,26 +15,44 @@ trap 'printf "ERROR: broker update failed at line %d.\n" "$LINENO" >&2' ERR
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
command -v git >/dev/null 2>&1 || fail "git is required" command -v git >/dev/null 2>&1 || fail "git is required"
command -v docker >/dev/null 2>&1 || fail "docker is required"
docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 is required"
[[ -f compose.yaml ]] || fail "compose.yaml was not found in ${SCRIPT_DIR}"
[[ -f .env ]] || fail ".env was not found in ${SCRIPT_DIR}"
if [[ "${1:-}" != "--post-pull" ]]; then if [[ "${1:-}" != "--post-pull" ]]; then
branch="$(git symbolic-ref --quiet --short HEAD)" || branch="$(git symbolic-ref --quiet --short HEAD)" ||
fail "the repository is in detached HEAD state" fail "the repository is in detached HEAD state"
git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1 || upstream="$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)" ||
fail "branch ${branch} does not have an upstream branch" fail "branch ${branch} does not have an upstream branch"
remote="$(git config --get "branch.${branch}.remote")" ||
fail "branch ${branch} does not have a configured remote"
git diff --quiet && git diff --cached --quiet || git diff --quiet && git diff --cached --quiet ||
fail "tracked local changes must be committed or stashed before updating" fail "tracked local changes must be committed or stashed before updating"
printf 'Updating TAPM Deployment Broker from branch %s...\n' "$branch" printf 'Checking %s against %s...\n' "$branch" "$upstream"
git pull --ff-only git fetch --prune "$remote"
local_commit="$(git rev-parse HEAD)"
upstream_commit="$(git rev-parse '@{upstream}')"
if [[ "$local_commit" == "$upstream_commit" ]]; then
printf 'TAPM Deployment Broker is already up to date; no changes were made.\n'
exit 0
fi
if git merge-base --is-ancestor "$local_commit" "$upstream_commit"; then
printf 'Updating TAPM Deployment Broker from %s...\n' "$upstream"
git merge --ff-only "$upstream_commit"
elif git merge-base --is-ancestor "$upstream_commit" "$local_commit"; then
fail "local branch ${branch} is ahead of ${upstream}; refusing to rebuild"
else
fail "local branch ${branch} has diverged from ${upstream}"
fi
# Restart from the newly pulled script so updates to this file take effect. # Restart from the newly pulled script so updates to this file take effect.
exec "$SCRIPT_PATH" --post-pull exec "$SCRIPT_PATH" --post-pull
fi fi
command -v docker >/dev/null 2>&1 || fail "docker is required"
docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 is required"
[[ -f compose.yaml ]] || fail "compose.yaml was not found in ${SCRIPT_DIR}"
[[ -f .env ]] || fail ".env was not found in ${SCRIPT_DIR}"
printf 'Building broker image...\n' printf 'Building broker image...\n'
docker compose build broker docker compose build broker