Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions cmd/api/handlers/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,16 @@ func (h *HandlersApi) PostureProfilesHandler(w http.ResponseWriter, r *http.Requ
apiErrorResponse(w, "missing auth context", http.StatusUnauthorized, nil)
return
}
utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, posture.AllProfiles())
if h.Posture == nil {
apiErrorResponse(w, "posture not configured", http.StatusServiceUnavailable, nil)
return
}
profiles, err := h.Posture.AllProfiles()
if err != nil {
apiErrorResponse(w, "error getting posture profiles", http.StatusInternalServerError, err)
return
}
utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, profiles)
}

// PostureProfileHandler — GET /api/v1/posture/profiles/{id}
Expand All @@ -723,8 +732,12 @@ func (h *HandlersApi) PostureProfileHandler(w http.ResponseWriter, r *http.Reque
apiErrorResponse(w, "profile id required", http.StatusBadRequest, nil)
return
}
profile := posture.GetProfile(profileID)
if profile == nil {
if h.Posture == nil {
apiErrorResponse(w, "posture not configured", http.StatusServiceUnavailable, nil)
return
}
profile, err := h.Posture.GetProfile(profileID)
if err != nil {
apiErrorResponse(w, "profile not found", http.StatusNotFound, nil)
return
}
Expand Down Expand Up @@ -854,7 +867,10 @@ func (h *HandlersApi) NodePostureScoreHandler(w http.ResponseWriter, r *http.Req
apiErrorResponse(w, "error getting posture", http.StatusInternalServerError, err)
return
}
calculator := posture.NewScoreCalculator()
score := calculator.Score(records)
score, err := h.Posture.Score(records)
if err != nil {
apiErrorResponse(w, "error scoring posture", http.StatusInternalServerError, err)
return
}
utils.HTTPResponse(w, utils.JSONApplicationUTF8, http.StatusOK, score)
}
2 changes: 1 addition & 1 deletion cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ const (
// API service config path
apiServiceConfigPath = "/service-config"
// API log sinks path
apiLogSinksPath = "/log-sinks"
apiLogSinksPath = "/log-sinks"
apiAuthProvidersPath = "/auth-providers"
// API features path
apiFeaturesPath = "/features"
Expand Down
6 changes: 5 additions & 1 deletion cmd/cli/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,11 @@ func postureProfileScheduleQueries(profileID, prefix string, intervalOverride in
if intervalOverride > 0 {
interval = intervalOverride
}
schedule[prefix+name] = environments.ScheduleQuery{
queryName := query.QueryName
if queryName == "" {
queryName = prefix + name
}
schedule[queryName] = environments.ScheduleQuery{
Query: query.Query,
Interval: json.Number(strconv.Itoa(interval)),
Platform: query.Platform,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ export interface NodePosture {
}

export interface ProfileQuery {
query_name?: string;
query: string;
interval: number;
platform?: string;
Expand Down
157 changes: 157 additions & 0 deletions frontend/src/features/environments/EnvConfigPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,163 @@ describe('EnvConfigPage', () => {
await user.click(await screen.findByRole('tab', { name: 'Schedule' }));

expect(screen.queryByRole('button', { name: 'Add posture checks' })).not.toBeInTheDocument();
expect(screen.queryByRole('tab', { name: 'Posture' })).not.toBeInTheDocument();
expect(mockGetPostureProfiles).not.toHaveBeenCalled();
});

it('adds a posture profile from the schedule picker into the posture tab', async () => {
const user = userEvent.setup();
mockGetFeatures.mockResolvedValue({ posture: true, service_config: false, accelerated: false, file_explorer: false });
mockGetPostureProfiles.mockResolvedValue([
{
id: 'linux-server',
name: 'Linux Servers',
description: 'Linux host posture checks',
platform: 'linux',
queries: {
users: {
query: 'SELECT username FROM users',
interval: 86400,
snapshot: true,
},
},
},
]);

renderWithProviders();

await user.click(await screen.findByRole('tab', { name: 'Schedule' }));
await user.click(screen.getByRole('button', { name: 'Add posture checks' }));

expect(await screen.findByText('linux')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Add Linux Servers to schedule' }));
await user.click(screen.getByRole('tab', { name: 'Posture' }));

expect(screen.getByDisplayValue('osctrl:posture:users')).toBeInTheDocument();
expect(screen.getByLabelText('Profile')).toHaveValue('linux-server');
});

it('saves the selected posture profile for a new manual check', async () => {
const user = userEvent.setup();
mockGetFeatures.mockResolvedValue({ posture: true, service_config: false, accelerated: false, file_explorer: false });
mockGetPostureProfiles.mockResolvedValue([
{
id: 'linux-server',
name: 'Linux Servers',
description: 'Linux host posture checks',
platform: 'linux',
queries: {},
},
]);
mockPatchConfig.mockImplementation(async (_env, body) => ({
options: '{}',
schedule: body.schedule,
packs: '{}',
decorators: '{}',
atc: '{}',
flags: '',
}));

renderWithProviders();

await user.click(await screen.findByRole('tab', { name: 'Posture' }));
await user.click(screen.getByRole('button', { name: 'Add check' }));
await user.selectOptions(screen.getByLabelText('Profile'), 'linux-server');
await user.click(screen.getByRole('button', { name: 'Save posture checks' }));

await waitFor(() => {
expect(mockPatchConfig).toHaveBeenCalledWith('dev', {
schedule: JSON.stringify({
'osctrl:posture:new_check': {
query: 'SELECT 1',
interval: 86400,
platform: 'linux',
snapshot: true,
profile_id: 'linux-server',
},
}, null, 2),
});
});
});

it('shows an environment-scoped posture tab when posture is enabled', async () => {
mockGetFeatures.mockResolvedValue({ posture: true, service_config: false, accelerated: false, file_explorer: false });
mockGetConfig.mockResolvedValue({
options: '{}',
schedule: JSON.stringify({
'osctrl:posture:users': {
query: 'SELECT username FROM users',
interval: 86400,
snapshot: true,
profile_id: 'linux-server',
},
}),
packs: '{}',
decorators: '{}',
atc: '{}',
flags: '',
});

renderWithProviders();

expect(await screen.findByRole('tab', { name: 'Posture' })).toBeInTheDocument();
});

it('hides the posture tab when posture is disabled', async () => {
renderWithProviders();

await screen.findByRole('tab', { name: 'Settings' });

expect(screen.queryByRole('tab', { name: 'Posture' })).not.toBeInTheDocument();
});

it('edits posture checks through the environment schedule', async () => {
const user = userEvent.setup();
mockGetFeatures.mockResolvedValue({ posture: true, service_config: false, accelerated: false, file_explorer: false });
mockGetConfig.mockResolvedValue({
options: '{}',
schedule: JSON.stringify({
'osctrl:posture:users': {
query: 'SELECT username FROM users',
interval: 86400,
snapshot: true,
profile_id: 'linux-server',
},
}),
packs: '{}',
decorators: '{}',
atc: '{}',
flags: '',
});
mockPatchConfig.mockImplementation(async (_env, body) => ({
options: '{}',
schedule: body.schedule,
packs: '{}',
decorators: '{}',
atc: '{}',
flags: '',
}));

renderWithProviders();

await user.click(await screen.findByRole('tab', { name: 'Posture' }));
await user.clear(screen.getByLabelText('Query name'));
await user.type(screen.getByLabelText('Query name'), 'osctrl:posture:interactive_users');
await user.clear(screen.getByLabelText('Interval'));
await user.type(screen.getByLabelText('Interval'), '3600');
await user.click(screen.getByRole('button', { name: 'Save posture checks' }));

await waitFor(() => {
expect(mockPatchConfig).toHaveBeenCalledWith('dev', {
schedule: JSON.stringify({
'osctrl:posture:interactive_users': {
query: 'SELECT username FROM users',
interval: 3600,
snapshot: true,
profile_id: 'linux-server',
},
}, null, 2),
});
});
});
});
Loading
Loading