update
This commit is contained in:
+7
-5
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user