Include the policy ID and name in the "script ran" activity of a script run queued by a policy failure (#22690)
#22692 # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] Added/updated tests - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [x] Manual QA for all new/changed functionality
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Record which policy automation triggered a script run in the activity feed
|
||||
@@ -887,6 +887,8 @@ This activity contains the following fields:
|
||||
- "script_execution_id": Execution ID of the script run.
|
||||
- "script_name": Name of the script (empty if it was an anonymous script).
|
||||
- "async": Whether the script was executed asynchronously.
|
||||
- "policy_id": ID of the policy whose failure triggered the script run. Null if no associated policy.
|
||||
- "policy_name": Name of the policy whose failure triggered the script run. Null if no associated policy.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -896,7 +898,9 @@ This activity contains the following fields:
|
||||
"host_display_name": "Anna's MacBook Pro",
|
||||
"script_name": "set-timezones.sh",
|
||||
"script_execution_id": "d6cffa75-b5b5-41ef-9230-15073c8a88cf",
|
||||
"async": false
|
||||
"async": false,
|
||||
"policy_id": 123,
|
||||
"policy_name": "Ensure photon torpedoes are primed"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var automationActivityAuthor = "Fleet"
|
||||
|
||||
// NewActivity stores an activity item that the user performed
|
||||
func (ds *Datastore) NewActivity(
|
||||
ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time,
|
||||
@@ -39,6 +41,10 @@ func (ds *Datastore) NewActivity(
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
} else if ranScriptActivity, ok := activity.(fleet.ActivityTypeRanScript); ok {
|
||||
if ranScriptActivity.PolicyID != nil {
|
||||
userName = &automationActivityAuthor
|
||||
}
|
||||
}
|
||||
|
||||
cols := []string{"user_id", "user_name", "activity_type", "details", "created_at"}
|
||||
@@ -293,7 +299,7 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
||||
// list pending scripts
|
||||
`SELECT
|
||||
hsr.execution_id as uuid,
|
||||
u.name as name,
|
||||
IF(hsr.policy_id IS NOT NULL, 'Fleet', u.name) as name,
|
||||
u.id as user_id,
|
||||
u.gravatar_url as gravatar_url,
|
||||
u.email as user_email,
|
||||
@@ -304,12 +310,16 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
||||
'host_display_name', COALESCE(hdn.display_name, ''),
|
||||
'script_name', COALESCE(scr.name, ''),
|
||||
'script_execution_id', hsr.execution_id,
|
||||
'async', NOT hsr.sync_request
|
||||
'async', NOT hsr.sync_request,
|
||||
'policy_id', hsr.policy_id,
|
||||
'policy_name', p.name
|
||||
) as details
|
||||
FROM
|
||||
host_script_results hsr
|
||||
LEFT OUTER JOIN
|
||||
users u ON u.id = hsr.user_id
|
||||
LEFT OUTER JOIN
|
||||
policies p ON p.id = hsr.policy_id
|
||||
LEFT OUTER JOIN
|
||||
host_display_names hdn ON hdn.host_id = hsr.host_id
|
||||
LEFT OUTER JOIN
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20241004005000, Down_20241004005000)
|
||||
}
|
||||
|
||||
func Up_20241004005000(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE host_script_results
|
||||
ADD COLUMN policy_id INT UNSIGNED DEFAULT NULL,
|
||||
ADD FOREIGN KEY fk_script_result_policy_id (policy_id) REFERENCES policies (id) ON DELETE SET NULL
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to add policy_id to host script results: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20241004005000(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20241004005000(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// insert a team
|
||||
teamID := execNoErrLastID(t, db, `INSERT INTO teams (name) VALUES ("Foo")`)
|
||||
|
||||
// insert a policy
|
||||
policyID := execNoErrLastID(t, db, `INSERT INTO policies (name, query, description, team_id, checksum)
|
||||
VALUES ('test_policy', "SELECT 1", "", ?, "a123b123")`, teamID)
|
||||
|
||||
// insert a script
|
||||
scriptContentID := execNoErrLastID(t, db, `INSERT INTO script_contents (md5_checksum, contents) VALUES ("md5", "echo 'Hello World'")`)
|
||||
scriptID := execNoErrLastID(t, db, `INSERT INTO scripts (
|
||||
team_id, global_or_team_id, name, script_content_id
|
||||
) VALUES (?, ?, "hello-world.sh", ?)`, teamID, teamID, scriptContentID)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// insert a script result
|
||||
hostScriptResultID := execNoErrLastID(t, db, `INSERT INTO host_script_results
|
||||
(host_id, execution_id, script_content_id, output, script_id, policy_id, user_id, sync_request)
|
||||
VALUES (1, 'a123b123', ?, '', ?, ?, NULL, FALSE)`, scriptContentID, scriptID, policyID)
|
||||
|
||||
// delete the associated policy
|
||||
execNoErr(t, db, `DELETE FROM policies WHERE id = ?`, policyID)
|
||||
|
||||
// policy ID should be null but script result should still exist
|
||||
var count int
|
||||
err := db.Get(&count, "SELECT COUNT(*) FROM host_script_results WHERE policy_id IS NULL AND id = ?", hostScriptResultID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func (ds *Datastore) PolicyLite(ctx context.Context, id uint) (*fleet.PolicyLite
|
||||
var policy fleet.PolicyLite
|
||||
err := sqlx.GetContext(
|
||||
ctx, ds.reader(ctx), &policy,
|
||||
`SELECT id, description, resolution FROM policies WHERE id=?`, id,
|
||||
`SELECT id, name, description, resolution FROM policies WHERE id=?`, id,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -39,8 +39,8 @@ func (ds *Datastore) NewHostScriptExecutionRequest(ctx context.Context, request
|
||||
|
||||
func newHostScriptExecutionRequest(ctx context.Context, tx sqlx.ExtContext, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) {
|
||||
const (
|
||||
insStmt = `INSERT INTO host_script_results (host_id, execution_id, script_content_id, output, script_id, user_id, sync_request) VALUES (?, ?, ?, '', ?, ?, ?)`
|
||||
getStmt = `SELECT hsr.id, hsr.host_id, hsr.execution_id, hsr.created_at, hsr.script_id, hsr.user_id, hsr.sync_request, sc.contents as script_contents FROM host_script_results hsr JOIN script_contents sc WHERE sc.id = hsr.script_content_id AND hsr.id = ?`
|
||||
insStmt = `INSERT INTO host_script_results (host_id, execution_id, script_content_id, output, script_id, policy_id, user_id, sync_request) VALUES (?, ?, ?, '', ?, ?, ?, ?)`
|
||||
getStmt = `SELECT hsr.id, hsr.host_id, hsr.execution_id, hsr.created_at, hsr.script_id, hsr.policy_id, hsr.user_id, hsr.sync_request, sc.contents as script_contents FROM host_script_results hsr JOIN script_contents sc WHERE sc.id = hsr.script_content_id AND hsr.id = ?`
|
||||
)
|
||||
|
||||
execID := uuid.New().String()
|
||||
@@ -49,6 +49,7 @@ func newHostScriptExecutionRequest(ctx context.Context, tx sqlx.ExtContext, requ
|
||||
execID,
|
||||
request.ScriptContentID,
|
||||
request.ScriptID,
|
||||
request.PolicyID,
|
||||
request.UserID,
|
||||
request.SyncRequest,
|
||||
)
|
||||
@@ -260,6 +261,7 @@ func (ds *Datastore) getHostScriptExecutionResultDB(ctx context.Context, q sqlx.
|
||||
hsr.execution_id,
|
||||
sc.contents as script_contents,
|
||||
hsr.script_id,
|
||||
hsr.policy_id,
|
||||
hsr.output,
|
||||
hsr.runtime,
|
||||
hsr.exit_code,
|
||||
|
||||
@@ -1166,11 +1166,13 @@ func (a ActivityTypeDisabledWindowsMDM) Documentation() (activity, details, deta
|
||||
}
|
||||
|
||||
type ActivityTypeRanScript struct {
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
ScriptExecutionID string `json:"script_execution_id"`
|
||||
ScriptName string `json:"script_name"`
|
||||
Async bool `json:"async"`
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
ScriptExecutionID string `json:"script_execution_id"`
|
||||
ScriptName string `json:"script_name"`
|
||||
Async bool `json:"async"`
|
||||
PolicyID *uint `json:"policy_id"`
|
||||
PolicyName *string `json:"policy_name"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeRanScript) ActivityName() string {
|
||||
@@ -1188,12 +1190,16 @@ func (a ActivityTypeRanScript) Documentation() (activity, details, detailsExampl
|
||||
- "host_display_name": Display name of the host.
|
||||
- "script_execution_id": Execution ID of the script run.
|
||||
- "script_name": Name of the script (empty if it was an anonymous script).
|
||||
- "async": Whether the script was executed asynchronously.`, `{
|
||||
- "async": Whether the script was executed asynchronously.
|
||||
- "policy_id": ID of the policy whose failure triggered the script run. Null if no associated policy.
|
||||
- "policy_name": Name of the policy whose failure triggered the script run. Null if no associated policy.`, `{
|
||||
"host_id": 1,
|
||||
"host_display_name": "Anna's MacBook Pro",
|
||||
"script_name": "set-timezones.sh",
|
||||
"script_execution_id": "d6cffa75-b5b5-41ef-9230-15073c8a88cf",
|
||||
"async": false
|
||||
"async": false,
|
||||
"policy_id": 123,
|
||||
"policy_name": "Ensure photon torpedoes are primed"
|
||||
}`
|
||||
}
|
||||
|
||||
|
||||
@@ -271,6 +271,8 @@ type PolicyScriptData struct {
|
||||
// PolicyLite is a stripped down version of the policy.
|
||||
type PolicyLite struct {
|
||||
ID uint `db:"id"`
|
||||
// Name is the name of the policy.
|
||||
Name string `db:"name"`
|
||||
// Description describes the policy.
|
||||
Description string `db:"description"`
|
||||
// Resolution describes how to solve a failing policy.
|
||||
|
||||
@@ -138,6 +138,7 @@ func (hs *HostScriptDetail) setLastExecution(executionID *string, executedAt *ti
|
||||
type HostScriptRequestPayload struct {
|
||||
HostID uint `json:"host_id"`
|
||||
ScriptID *uint `json:"script_id"`
|
||||
PolicyID *uint `json:"policy_id"`
|
||||
ScriptContents string `json:"script_contents"`
|
||||
ScriptContentID uint `json:"-"`
|
||||
ScriptName string `json:"script_name"`
|
||||
@@ -217,6 +218,9 @@ type HostScriptResult struct {
|
||||
// ScriptID is the id of the saved script to execute, or nil if this was an
|
||||
// anonymous script execution.
|
||||
ScriptID *uint `json:"script_id" db:"script_id"`
|
||||
// PolicyID is the id of the policy that triggered the script execution, or
|
||||
// nil if the execution was not triggered by a policy failure
|
||||
PolicyID *uint `json:"policy_id" db:"policy_id"`
|
||||
// UserID is the id of the user that requested execution. It is not part of
|
||||
// the rendered JSON as it is only returned by the
|
||||
// /hosts/:id/activities/upcoming endpoint which doesn't use this struct as
|
||||
|
||||
@@ -67,6 +67,8 @@ func (svc *Service) NewActivity(ctx context.Context, user *fleet.User, activity
|
||||
return newActivity(ctx, user, activity, svc.ds, svc.logger)
|
||||
}
|
||||
|
||||
var automationActivityAuthor = "Fleet"
|
||||
|
||||
func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, ds fleet.Datastore, logger kitlog.Logger) error {
|
||||
appConfig, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
@@ -84,6 +86,8 @@ func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityD
|
||||
var userID *uint
|
||||
var userName *string
|
||||
var userEmail *string
|
||||
activityType := activity.ActivityName()
|
||||
|
||||
if user != nil {
|
||||
// To support creating activities with users that were deleted. This can happen
|
||||
// for automatically installed software which uses the author of the upload as the author of
|
||||
@@ -93,8 +97,12 @@ func newActivity(ctx context.Context, user *fleet.User, activity fleet.ActivityD
|
||||
}
|
||||
userName = &user.Name
|
||||
userEmail = &user.Email
|
||||
} else if ranScriptActivity, ok := activity.(fleet.ActivityTypeRanScript); ok {
|
||||
if ranScriptActivity.PolicyID != nil {
|
||||
userName = &automationActivityAuthor
|
||||
}
|
||||
}
|
||||
activityType := activity.ActivityName()
|
||||
|
||||
go func() {
|
||||
retryStrategy := backoff.NewExponentialBackOff()
|
||||
retryStrategy.MaxElapsedTime = 30 * time.Minute
|
||||
|
||||
@@ -6284,7 +6284,7 @@ func (s *integrationEnterpriseTestSuite) TestRunHostSavedScript() {
|
||||
s.lastActivityMatches(
|
||||
fleet.ActivityTypeRanScript{}.ActivityName(),
|
||||
fmt.Sprintf(
|
||||
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true}`,
|
||||
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null}`,
|
||||
host.ID, host.DisplayName(), savedNoTmScript.Name, scriptResultResp.ExecutionID,
|
||||
),
|
||||
0,
|
||||
@@ -12196,7 +12196,7 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptSoftDelete() {
|
||||
s.lastActivityOfTypeMatches(
|
||||
fleet.ActivityTypeRanScript{}.ActivityName(),
|
||||
fmt.Sprintf(
|
||||
`{"host_id": %d, "host_display_name": %q, "script_name": "", "script_execution_id": %q, "async": true}`,
|
||||
`{"host_id": %d, "host_display_name": %q, "script_name": "", "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null}`,
|
||||
host.ID, host.DisplayName(), scriptExecID), 0)
|
||||
|
||||
// create a saved script execution request
|
||||
@@ -12218,7 +12218,7 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptSoftDelete() {
|
||||
http.StatusOK)
|
||||
s.lastActivityOfTypeMatches(
|
||||
fleet.ActivityTypeRanScript{}.ActivityName(),
|
||||
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "script_name": "script1.sh", "script_execution_id": %q, "async": true}`,
|
||||
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "script_name": "script1.sh", "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null}`,
|
||||
host.ID, host.DisplayName(), savedScriptExecID), 0)
|
||||
|
||||
// get the anoymous script result details
|
||||
@@ -14717,11 +14717,12 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsScripts() {
|
||||
},
|
||||
), http.StatusOK, &distributedResp)
|
||||
|
||||
hostPendingScript, err = s.ds.IsExecutionPendingForHost(ctx, host3Team2.ID, psScript.ID)
|
||||
host3PendingScripts, err := s.ds.ListPendingHostScriptExecutions(ctx, host3Team2.ID)
|
||||
require.NoError(t, err)
|
||||
require.True(t, hostPendingScript)
|
||||
require.Len(t, host3PendingScripts, 1)
|
||||
host3executionID := host3PendingScripts[0].ExecutionID
|
||||
|
||||
// Unassociate policy4Team2 from script.
|
||||
// Dissociate policy4Team2 from script.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team2.ID, policy4Team2.ID), modifyTeamPolicyRequest{
|
||||
ModifyPolicyPayload: fleet.ModifyPolicyPayload{
|
||||
@@ -14750,6 +14751,38 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsScripts() {
|
||||
hostPendingScripts, err := s.ds.ListPendingHostScriptExecutions(ctx, hostVanillaOsquery5Team1.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hostPendingScripts, 0)
|
||||
|
||||
// activity feed should show script run as pending, with "Fleet" as author, policy ID and name set in body
|
||||
var listResp listActivitiesResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", host3Team2.ID), nil, http.StatusOK, &listResp)
|
||||
require.Len(t, listResp.Activities, 1)
|
||||
require.Nil(t, listResp.Activities[0].ActorEmail)
|
||||
require.Equal(t, "Fleet", *listResp.Activities[0].ActorFullName)
|
||||
require.Nil(t, listResp.Activities[0].ActorGravatar)
|
||||
require.Equal(t, "ran_script", listResp.Activities[0].Type)
|
||||
var activityJson map[string]interface{}
|
||||
err = json.Unmarshal(*listResp.Activities[0].Details, &activityJson)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, float64(policy4Team2.ID), activityJson["policy_id"])
|
||||
require.Equal(t, "policy4Team2", activityJson["policy_name"])
|
||||
|
||||
// post script result response
|
||||
var orbitPostScriptResp orbitPostScriptResultResponse
|
||||
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
|
||||
json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *host3Team2.OrbitNodeKey, host3executionID)),
|
||||
http.StatusOK, &orbitPostScriptResp)
|
||||
|
||||
// activity feed should show script run as completed
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", host3Team2.ID), nil, http.StatusOK, &listResp)
|
||||
require.Len(t, listResp.Activities, 1)
|
||||
require.Equal(t, "", *listResp.Activities[0].ActorEmail) // actor email is blank rather than nil here 👀
|
||||
require.Equal(t, "Fleet", *listResp.Activities[0].ActorFullName)
|
||||
require.Nil(t, listResp.Activities[0].ActorGravatar)
|
||||
require.Equal(t, "ran_script", listResp.Activities[0].Type)
|
||||
err = json.Unmarshal(*listResp.Activities[0].Details, &activityJson)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, float64(policy4Team2.ID), activityJson["policy_id"])
|
||||
require.Equal(t, "policy4Team2", activityJson["policy_name"])
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestSoftwareInstallersWithoutBundleIdentifier() {
|
||||
|
||||
@@ -733,6 +733,13 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host
|
||||
}
|
||||
default:
|
||||
// TODO(sarah): We may need to special case lock/unlock script results here?
|
||||
var policyName *string
|
||||
if hsr.PolicyID != nil {
|
||||
if policy, err := svc.ds.PolicyLite(ctx, *hsr.PolicyID); err == nil {
|
||||
policyName = &policy.Name // fall back to blank policy name if we can't retrieve the policy
|
||||
}
|
||||
}
|
||||
|
||||
if err := svc.NewActivity(
|
||||
ctx,
|
||||
user,
|
||||
@@ -742,6 +749,8 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host
|
||||
ScriptExecutionID: hsr.ExecutionID,
|
||||
ScriptName: scriptName,
|
||||
Async: !hsr.SyncRequest,
|
||||
PolicyID: hsr.PolicyID,
|
||||
PolicyName: policyName,
|
||||
},
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for script execution request")
|
||||
|
||||
@@ -1931,6 +1931,8 @@ func (svc *Service) processScriptsForNewlyFailingPolicies(
|
||||
}
|
||||
|
||||
for _, failingPolicyWithScript := range failingPoliciesWithScript {
|
||||
policyID := failingPolicyWithScript.ID
|
||||
|
||||
scriptMetadata, err := svc.ds.Script(ctx, failingPolicyWithScript.ScriptID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get script metadata by id")
|
||||
@@ -1938,7 +1940,7 @@ func (svc *Service) processScriptsForNewlyFailingPolicies(
|
||||
logger := log.With(svc.logger,
|
||||
"host_id", hostID,
|
||||
"host_platform", hostPlatform,
|
||||
"policy_id", failingPolicyWithScript.ID,
|
||||
"policy_id", policyID,
|
||||
"script_id", failingPolicyWithScript.ScriptID,
|
||||
"script_name", scriptMetadata.Name,
|
||||
)
|
||||
@@ -1989,6 +1991,7 @@ func (svc *Service) processScriptsForNewlyFailingPolicies(
|
||||
ScriptContentID: scriptMetadata.ScriptContentID,
|
||||
ScriptID: &scriptMetadata.ID,
|
||||
TeamID: policyTeamID,
|
||||
PolicyID: &policyID,
|
||||
// no user ID as scripts are executed by Fleet
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user