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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,39 @@ yarn start privmon-quick --space default
yarn start csp --data-sources all --findings-count 50
```

### Correlated Organization Data

Interactive — prompts for size, productivity suite, and detection rules:

```bash
yarn start org-data
```

Non-interactive — skip all prompts by providing options directly:

```bash
yarn start org-data --size small --productivity-suite microsoft --integrations azure,entra_id
```

Enable detection rules without prompting:

```bash
yarn start org-data --size medium --detection-rules
```

Each prompt is skipped individually when its flag is present. Omit any flag to be prompted for it:

| Flag | Values | Default (when omitted) |
| ---------------------- | ------------------------------------------- | ------------------------ |
| `--size` | `john-doe`, `small`, `medium`, `enterprise` | interactive prompt |
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README documents --size values as john-doe, but the implementation uses john_doe (underscore). Using the documented value will lead to an invalid size and can break generation (e.g. size-based config lookups). Update the docs to match the actual accepted value, or implement an alias mapping in the CLI.

Suggested change
| `--size` | `john-doe`, `small`, `medium`, `enterprise` | interactive prompt |
| `--size` | `john_doe`, `small`, `medium`, `enterprise` | interactive prompt |

Copilot uses AI. Check for mistakes.
| `--productivity-suite` | `microsoft`, `google` | interactive prompt |
| `--detection-rules` | flag (boolean) | interactive prompt |
| `--integrations` | comma-separated list | all default integrations |
| `--all` | flag (boolean) | — |
| `--name` | string | `Acme CRM` |
| `--space` | string | `default` |
| `--seed` | number | random |

## Commands

Detailed command documentation is colocated with command code under `src/commands`.
Expand Down
11 changes: 10 additions & 1 deletion src/commands/org_data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,26 @@ export const orgDataCommands: CommandModule = {
)
.option('--all', 'Generate all integrations regardless of company size')
.option('--detection-rules', 'Include sample detection rules for applicable integrations')
.option(
'--size <size>',
'Organization size without prompting (john-doe|small|medium|enterprise)',
)
Comment on lines +23 to +26
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI help says --size accepts john-doe, but the codebase uses OrganizationSize = 'john_doe' | ... (underscore). Passing john-doe will propagate an invalid size value and can break lookups like SIZE_CONFIGS[size]. Update the help text (and README) to john_doe, or add normalization/validation to accept john-doe and map it to john_doe (ideally using Commander .choices(...)).

Copilot uses AI. Check for mistakes.
.option(
'--productivity-suite <suite>',
'Productivity suite without prompting (microsoft|google)',
)
.addHelpText('after', '\n' + getOrgDataHelp())
.action(
wrapAction(async (options) => {
await runOrgData({
size: 'medium',
size: options.size,
name: options.name,
space: options.space,
seed: options.seed,
integrations: options.integrations,
all: options.all,
detectionRules: options.detectionRules,
productivitySuite: options.productivitySuite,
});
}),
);
Expand Down
66 changes: 36 additions & 30 deletions src/commands/org_data/integrations/azure_integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,29 +450,48 @@ export class AzureIntegration extends BaseIntegration {
const auditCount = Math.max(5, Math.ceil(employees.length * 0.3));

for (let i = 0; i < auditCount; i++) {
const employee = faker.helpers.arrayElement(employees);
docs.push(this.createAuditLogDoc(employee, tenantId));
const actor = faker.helpers.arrayElement(employees);
docs.push(this.createAuditLogDoc(actor, employees, tenantId));
}

return docs;
}

private createAuditLogDoc(employee: Employee, tenantId: string): IntegrationDocument {
private createAuditLogDoc(
actor: Employee,
employees: Employee[],
tenantId: string,
): IntegrationDocument {
const timestamp = this.getRandomTimestamp(72);
const activity = faker.helpers.weightedArrayElement(
AUDIT_LOG_ACTIVITIES.map((a) => ({ value: a, weight: a.weight })),
);
const correlationId = faker.string.uuid();
const targetId = faker.string.uuid();
const targetDisplayName =
activity.targetType === 'User'
? `${employee.firstName} ${employee.lastName}`
: activity.targetType === 'Device'
? `DESKTOP-${faker.string.alphanumeric(7).toUpperCase()}`
: `${activity.category}-${faker.string.alphanumeric(6)}`;

const isAppInitiated = faker.datatype.boolean();
// Pick a distinct target employee for User-type relationships
const otherEmployees = employees.filter((e) => e.email !== actor.email);
const targetEmployee =
otherEmployees.length > 0 ? faker.helpers.arrayElement(otherEmployees) : actor;

// Build the target resource. User-type targets need userPrincipalName so the
// ingest pipeline maps it to target_resources.0.user_principal_name, which is
// what the communicates_with ES|QL query reads for user→user relationships.
const targetResource: Record<string, unknown> = {
id: targetId,
type: activity.targetType,
};
if (activity.targetType === 'User') {
targetResource.displayName = `${targetEmployee.firstName} ${targetEmployee.lastName}`;
targetResource.userPrincipalName = targetEmployee.email;
} else if (activity.targetType === 'Device') {
targetResource.displayName = `DESKTOP-${faker.string.alphanumeric(7).toUpperCase()}`;
} else {
targetResource.displayName = `${activity.category}-${faker.string.alphanumeric(6)}`;
}

// Always user-initiated so the actor UPN (initiated_by.user.userPrincipalName)
// is present and can be used to build the actor EUID (user:{email}@entra_id).
const rawAzureJson = {
time: timestamp,
category: 'AuditLogs',
Expand All @@ -491,27 +510,14 @@ export class AzureIntegration extends BaseIntegration {
loggedByService: activity.loggedByService,
operationType: activity.operationType,
resultReason: '',
targetResources: [
{
displayName: targetDisplayName,
id: targetId,
type: activity.targetType,
targetResources: [targetResource],
initiatedBy: {
user: {
displayName: `${actor.firstName} ${actor.lastName}`,
userPrincipalName: actor.email,
id: actor.entraIdUserId,
},
],
initiatedBy: isAppInitiated
? {
app: {
displayName: 'Device Registration Service',
servicePrincipalId: faker.string.uuid(),
},
}
: {
user: {
displayName: `${employee.firstName} ${employee.lastName}`,
userPrincipalName: employee.email,
id: employee.entraIdUserId,
},
},
},
},
};

Expand Down
Loading