Skip to content

Commit 1df3a92

Browse files
authored
Merge pull request #2904 from trycompai/main
[comp] Production Deploy
2 parents e1234c5 + 10bcf72 commit 1df3a92

29 files changed

Lines changed: 1926 additions & 301 deletions

‎apps/api/src/assistant-chat/assistant-chat.controller.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { AssistantChatService } from './assistant-chat.service';
3939
import { buildTools } from './assistant-chat-tools';
4040
import type { AssistantChatMessage } from './assistant-chat.types';
4141
import { RolesService } from '../roles/roles.service';
42+
import { ASSISTANT_OPENAI_PROVIDER_OPTIONS } from './openai-options';
4243

4344
@ApiTags('Assistant Chat')
4445
@Controller({ path: 'assistant-chat', version: '1' })
@@ -129,6 +130,7 @@ Important:
129130
system: systemPrompt,
130131
messages: await convertToModelMessages(messages),
131132
tools,
133+
providerOptions: ASSISTANT_OPENAI_PROVIDER_OPTIONS,
132134
stopWhen: stepCountIs(5),
133135
});
134136

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { ASSISTANT_OPENAI_PROVIDER_OPTIONS } from './openai-options';
2+
3+
describe('ASSISTANT_OPENAI_PROVIDER_OPTIONS', () => {
4+
it('disables stored Responses API item references for assistant chat', () => {
5+
expect(ASSISTANT_OPENAI_PROVIDER_OPTIONS.openai.store).toBe(false);
6+
});
7+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { OpenAIResponsesProviderOptions } from '@ai-sdk/openai';
2+
3+
export const ASSISTANT_OPENAI_PROVIDER_OPTIONS = {
4+
openai: {
5+
store: false,
6+
} satisfies OpenAIResponsesProviderOptions,
7+
};

‎apps/api/src/cloud-security/ai-remediation.service.ts‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export class AiRemediationService {
5959
this.logger.log(
6060
`AI plan for ${finding.findingKey}: canAutoFix=${object.canAutoFix}, risk=${object.risk}`,
6161
);
62-
return normalizeFixPlan(enrichEmptyState(object));
62+
return normalizeFixPlan(enrichEmptyState(object), {
63+
resourceId: finding.resourceId,
64+
});
6365
} catch (err) {
6466
this.logger.error(
6567
`AI plan failed: ${err instanceof Error ? err.message : String(err)}`,
@@ -101,13 +103,17 @@ Generate the complete fix plan with EXACT values from the real AWS state.`,
101103
});
102104

103105
this.logger.log(`AI refined plan for ${params.finding.findingKey}`);
104-
return normalizeFixPlan(enrichEmptyState(object));
106+
return normalizeFixPlan(enrichEmptyState(object), {
107+
resourceId: params.finding.resourceId,
108+
});
105109
} catch (err) {
106110
this.logger.error(
107111
`AI refine failed: ${err instanceof Error ? err.message : String(err)}`,
108112
);
109113
// Fall back to original plan
110-
return normalizeFixPlan(enrichEmptyState(params.originalPlan));
114+
return normalizeFixPlan(enrichEmptyState(params.originalPlan), {
115+
resourceId: params.finding.resourceId,
116+
});
111117
}
112118
}
113119

@@ -252,7 +258,11 @@ OVERESTIMATE. Better to have 5 extra permissions than to miss one.`,
252258
const neighbors = [
253259
...params.planContext.readSteps.map((s) => ({ role: 'read', ...s })),
254260
...params.planContext.fixSteps.map((s) => ({ role: 'fix', ...s })),
255-
].filter((s) => s.command !== params.step.command || s.purpose !== params.step.purpose);
261+
].filter(
262+
(s) =>
263+
s.command !== params.step.command ||
264+
s.purpose !== params.step.purpose,
265+
);
256266

257267
const { object } = await generateObject({
258268
model: MODEL,
@@ -475,9 +485,7 @@ Generate the complete fix plan with EXACT values from the real Azure state.`,
475485

476486
const steps: string[] = [];
477487
if (externalUri) {
478-
steps.push(
479-
`Open the resource in GCP Console: ${externalUri}`,
480-
);
488+
steps.push(`Open the resource in GCP Console: ${externalUri}`);
481489
}
482490
if (finding.remediation) {
483491
// Split SCC remediation text into separate steps if it contains "More info:" or multiple sentences
@@ -579,7 +587,9 @@ function enrichEmptyState(plan: FixPlan): FixPlan {
579587
const command = typeof step?.command === 'string' ? step.command : '';
580588
const prefix = ACTIONABLE_PREFIXES.find((p) => command.startsWith(p));
581589
if (!prefix) continue;
582-
const resource = command.replace(/Command$/, '').replace(/^[A-Z][a-z]+/, '');
590+
const resource = command
591+
.replace(/Command$/, '')
592+
.replace(/^[A-Z][a-z]+/, '');
583593
const label = step.service ? `${step.service}:${resource}` : resource;
584594
if (prefix === 'Create') {
585595
if (!willCreate.includes(label)) willCreate.push(label);

‎apps/api/src/cloud-security/aws-command-executor.spec.ts‎

Lines changed: 127 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ describe('validatePlanSteps — REQUIRED_PARAMS', () => {
4545
params: { AWSServiceName: 'config.amazonaws.com' },
4646
}),
4747
]);
48-
expect(
49-
errors.filter((e) => e.includes('AWSServiceName')),
50-
).toHaveLength(0);
48+
expect(errors.filter((e) => e.includes('AWSServiceName'))).toHaveLength(0);
5149
});
5250

5351
it.each(['', null, undefined])(
@@ -84,6 +82,126 @@ describe('validatePlanSteps — REQUIRED_PARAMS', () => {
8482
).toHaveLength(1);
8583
});
8684

85+
it('requires a group identifier for property-based security-group revoke commands', () => {
86+
const errors = validatePlanSteps([
87+
step({
88+
service: 'ec2',
89+
command: 'RevokeSecurityGroupIngressCommand',
90+
params: {
91+
IpPermissions: [
92+
{
93+
IpProtocol: 'tcp',
94+
FromPort: 22,
95+
ToPort: 22,
96+
IpRanges: [{ CidrIp: '0.0.0.0/0' }],
97+
},
98+
],
99+
},
100+
}),
101+
]);
102+
103+
expect(errors).toEqual(
104+
expect.arrayContaining([
105+
'Step 1 (RevokeSecurityGroupIngressCommand): One of "GroupId" or "GroupName" is required',
106+
]),
107+
);
108+
});
109+
110+
it('rejects revoke commands that mix rule IDs with rule properties', () => {
111+
const errors = validatePlanSteps([
112+
step({
113+
service: 'ec2',
114+
command: 'RevokeSecurityGroupIngressCommand',
115+
params: {
116+
GroupId: 'sg-0123abc',
117+
SecurityGroupRuleIds: ['sgr-0123abc'],
118+
IpPermissions: [
119+
{
120+
IpProtocol: 'tcp',
121+
FromPort: 22,
122+
ToPort: 22,
123+
IpRanges: [{ CidrIp: '0.0.0.0/0' }],
124+
},
125+
],
126+
},
127+
}),
128+
]);
129+
130+
expect(errors).toEqual(
131+
expect.arrayContaining([
132+
'Step 1 (RevokeSecurityGroupIngressCommand): SecurityGroupRuleIds cannot be combined with rule property params',
133+
]),
134+
);
135+
});
136+
137+
it('requires a rule selector for security-group revoke commands', () => {
138+
const errors = validatePlanSteps([
139+
step({
140+
service: 'ec2',
141+
command: 'RevokeSecurityGroupIngressCommand',
142+
params: { GroupId: 'sg-0123abc' },
143+
}),
144+
]);
145+
146+
expect(errors).toEqual(
147+
expect.arrayContaining([
148+
'Step 1 (RevokeSecurityGroupIngressCommand): One of "SecurityGroupRuleIds" or rule property params is required',
149+
]),
150+
);
151+
});
152+
153+
it('allows property-based security-group revoke commands when GroupId is present', () => {
154+
const errors = validatePlanSteps([
155+
step({
156+
service: 'ec2',
157+
command: 'RevokeSecurityGroupIngressCommand',
158+
params: {
159+
GroupId: 'sg-0123abc',
160+
IpPermissions: [
161+
{
162+
IpProtocol: 'tcp',
163+
FromPort: 22,
164+
ToPort: 22,
165+
IpRanges: [{ CidrIp: '0.0.0.0/0' }],
166+
},
167+
],
168+
},
169+
}),
170+
]);
171+
172+
expect(
173+
errors.some((e) => /RevokeSecurityGroupIngressCommand/.test(e)),
174+
).toBe(false);
175+
});
176+
177+
it('allows security-group revoke commands that use SecurityGroupRuleIds only', () => {
178+
const errors = validatePlanSteps([
179+
step({
180+
service: 'ec2',
181+
command: 'RevokeSecurityGroupIngressCommand',
182+
params: { SecurityGroupRuleIds: ['sgr-0123abc'] },
183+
}),
184+
]);
185+
186+
expect(errors.some((e) => /GroupId|GroupName/.test(e))).toBe(false);
187+
});
188+
189+
it('treats empty one-of arrays as missing values', () => {
190+
const errors = validatePlanSteps([
191+
step({
192+
service: 'ec2',
193+
command: 'RevokeSecurityGroupIngressCommand',
194+
params: { SecurityGroupRuleIds: [] },
195+
}),
196+
]);
197+
198+
expect(errors).toEqual(
199+
expect.arrayContaining([
200+
'Step 1 (RevokeSecurityGroupIngressCommand): One of "SecurityGroupRuleIds" or rule property params is required',
201+
]),
202+
);
203+
});
204+
87205
it('does NOT apply required-param checks to commands not in REQUIRED_PARAMS', () => {
88206
// PutBucketVersioningCommand isn't in REQUIRED_PARAMS — should pass
89207
// even with no params (the AWS SDK will surface its own errors then).
@@ -101,7 +219,11 @@ describe('validatePlanSteps — REQUIRED_PARAMS', () => {
101219

102220
it('uses the step index in the error message so customers know which step is broken', () => {
103221
const errors = validatePlanSteps([
104-
step({ service: 's3', command: 'PutBucketVersioningCommand', params: { Bucket: 'b', VersioningConfiguration: { Status: 'Enabled' } } }),
222+
step({
223+
service: 's3',
224+
command: 'PutBucketVersioningCommand',
225+
params: { Bucket: 'b', VersioningConfiguration: { Status: 'Enabled' } },
226+
}),
105227
step({
106228
service: 'iam',
107229
command: 'CreateServiceLinkedRoleCommand',
@@ -139,6 +261,7 @@ describe('looksLikeValidationError', () => {
139261
'Member must not be null',
140262
'failed to satisfy constraint: Member must have length less than or equal to 64',
141263
'Missing required parameter Bucket',
264+
'The request must contain the parameter groupName or groupId',
142265
'is required',
143266
'must be a valid ARN',
144267
])('detects %p as a validation-class error', (msg) => {

‎apps/api/src/cloud-security/aws-command-executor.ts‎

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,20 @@ export const REQUIRED_PARAMS: Record<string, readonly string[]> = {
167167
CreateTrailCommand: ['Name', 'S3BucketName'],
168168
};
169169

170+
const REQUIRED_PARAM_ONE_OF: Record<string, readonly (readonly string[])[]> = {
171+
AuthorizeSecurityGroupIngressCommand: [['GroupId', 'GroupName']],
172+
};
173+
174+
const REVOKE_SECURITY_GROUP_INGRESS_RULE_PROPERTY_PARAMS = [
175+
'CidrIp',
176+
'FromPort',
177+
'IpPermissions',
178+
'IpProtocol',
179+
'SourceSecurityGroupName',
180+
'SourceSecurityGroupOwnerId',
181+
'ToPort',
182+
] as const;
183+
170184
function normalizeArnPartition(value: string, partition: AwsPartition): string {
171185
if (partition === 'aws-us-gov') {
172186
return value.replace(/\barn:aws:/g, 'arn:aws-us-gov:');
@@ -236,8 +250,9 @@ function normaliseInputParams(
236250

237251
// Rule 2: S3 CreateBucket needs LocationConstraint for non-us-east-1
238252
if (command === 'CreateBucketCommand') {
239-
if (input.Bucket) {
240-
input.Bucket = String(input.Bucket).toLowerCase().replace(/_/g, '-');
253+
const bucket = input.Bucket;
254+
if (typeof bucket === 'string' || typeof bucket === 'number') {
255+
input.Bucket = String(bucket).toLowerCase().replace(/_/g, '-');
241256
}
242257
if (region !== 'us-east-1' && !input.CreateBucketConfiguration) {
243258
input.CreateBucketConfiguration = { LocationConstraint: region };
@@ -412,7 +427,8 @@ export function looksLikeValidationError(message: string): boolean {
412427
lower.includes('invalid parameter') ||
413428
lower.includes('must be a valid') ||
414429
lower.includes('is required') ||
415-
lower.includes('missing required')
430+
lower.includes('missing required') ||
431+
lower.includes('must contain')
416432
);
417433
}
418434

@@ -533,18 +549,73 @@ export function validatePlanSteps(steps: AwsCommandStep[]): string[] {
533549
if (required) {
534550
for (const key of required) {
535551
const value = step.params?.[key];
536-
if (value === undefined || value === null || value === '') {
552+
if (!hasRequiredParamValue(value)) {
553+
errors.push(`${prefix}: Required param "${key}" is missing or empty`);
554+
}
555+
}
556+
}
557+
558+
const oneOfGroups = REQUIRED_PARAM_ONE_OF[step.command];
559+
if (oneOfGroups) {
560+
for (const group of oneOfGroups) {
561+
const hasAny = group.some((key) => {
562+
const value = step.params?.[key];
563+
return hasRequiredParamValue(value);
564+
});
565+
if (!hasAny) {
537566
errors.push(
538-
`${prefix}: Required param "${key}" is missing or empty`,
567+
`${prefix}: One of "${group.join('" or "')}" is required`,
539568
);
540569
}
541570
}
542571
}
572+
573+
if (step.command === 'RevokeSecurityGroupIngressCommand') {
574+
errors.push(...validateRevokeSecurityGroupIngressParams(step, prefix));
575+
}
543576
}
544577

545578
return errors;
546579
}
547580

581+
function validateRevokeSecurityGroupIngressParams(
582+
step: AwsCommandStep,
583+
prefix: string,
584+
): string[] {
585+
const params = step.params ?? {};
586+
const hasGroupIdentifier =
587+
hasRequiredParamValue(params.GroupId) ||
588+
hasRequiredParamValue(params.GroupName);
589+
const hasRuleIds = hasRequiredParamValue(params.SecurityGroupRuleIds);
590+
const hasRuleProperties =
591+
REVOKE_SECURITY_GROUP_INGRESS_RULE_PROPERTY_PARAMS.some((key) =>
592+
hasRequiredParamValue(params[key]),
593+
);
594+
595+
if (!hasRuleIds && !hasRuleProperties) {
596+
return [
597+
`${prefix}: One of "SecurityGroupRuleIds" or rule property params is required`,
598+
];
599+
}
600+
601+
if (hasRuleIds && hasRuleProperties) {
602+
return [
603+
`${prefix}: SecurityGroupRuleIds cannot be combined with rule property params`,
604+
];
605+
}
606+
607+
if (hasRuleIds && !hasRuleProperties) return [];
608+
if (hasGroupIdentifier) return [];
609+
610+
return [`${prefix}: One of "GroupId" or "GroupName" is required`];
611+
}
612+
613+
function hasRequiredParamValue(value: unknown): boolean {
614+
if (value === undefined || value === null || value === '') return false;
615+
if (Array.isArray(value)) return value.length > 0;
616+
return true;
617+
}
618+
548619
export interface StepResult {
549620
step: AwsCommandStep;
550621
output: Record<string, unknown>;

0 commit comments

Comments
 (0)