Skip to content
Open
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: 26 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "identify-removed-users",
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "tsx",
"program": "${workspaceFolder}/src/index.ts",
"args": ["identify-removed-users"],
"sourceMaps": true,
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"type": "node",
"request": "launch",
"name": "restore-removed-users",
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "tsx",
"program": "${workspaceFolder}/src/index.ts",
"args": ["restore-removed-users"],
"sourceMaps": true,
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
130 changes: 130 additions & 0 deletions src/api/audit-log-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Octokit } from 'octokit';

/*

action:repo.remove_member
org:software
created:>=2025-11-17T22:00:00Z
created:<2025-11-17T23:00:00Z
repo:software/dtc-release-cicd

*/
/**
* Get audit log activity for a repository filtered by usernames and optional criteria
* @param octokit - The Octokit instance
* @param orgName - The organization name
* @param repoName - The repository name
* @param usernames - Array of usernames to filter audit log events by
* @param sinceIso - Optional ISO date string to filter events after this date
* @param action - Optional action type to filter by (e.g., "repo.create", "repo.destroy")
* @param created - Optional creation date filter (e.g., ">=2025-11-17T22:00:00Z")
* @param include - Filter by event source: "all", "web", or "git". Defaults to "all"
* @yields Objects containing actor username, action type, and event date
Comment on lines +15 to +22
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

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

Outdated documentation. The JSDoc comment does not mention the enterpriseName, type, created, or include parameters, but these are part of the function signature. The documentation should be updated to reflect all parameters.

Suggested change
* @param orgName - The organization name
* @param repoName - The repository name
* @param usernames - Array of usernames to filter audit log events by
* @param sinceIso - Optional ISO date string to filter events after this date
* @param action - Optional action type to filter by (e.g., "repo.create", "repo.destroy")
* @param created - Optional creation date filter (e.g., ">=2025-11-17T22:00:00Z")
* @param include - Filter by event source: "all", "web", or "git". Defaults to "all"
* @yields Objects containing actor username, action type, and event date
* @param orgName - The organization name (required if type is "org")
* @param enterpriseName - The enterprise name (required if type is "enterprise")
* @param repoName - The repository name
* @param usernames - Array of usernames to filter audit log events by
* @param sinceIso - Optional ISO date string to filter events after this date
* @param action - Optional action type or array of action types to filter by (e.g., "repo.create", "repo.destroy")
* @param created - Optional creation date filter or array of filters (e.g., ">=2025-11-17T22:00:00Z")
* @param include - Filter by event source: "all", "web", or "git". Defaults to "all"
* @param type - The type of audit log to query: "org" or "enterprise"
* @yields Objects containing actor username, action type, event date, and event properties

Copilot uses AI. Check for mistakes.
*/
export async function* getAuditLogActivity({
octokit,
orgName,
enterpriseName,
repoName,
usernames,
sinceIso,
action,
created,
include = 'all',
type = 'org',
}: {
octokit: Octokit;
orgName?: string;
enterpriseName?: string;
repoName?: string;
usernames?: string[];
sinceIso?: string;
action?: string | string[];
created?: string | string[];
include?: 'all' | 'web' | 'git';
type: 'org' | 'enterprise';
}): AsyncGenerator<{ actor: string; action: string; date: Date; props: any }> {
if (type == 'org' && !orgName) {
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

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

Use strict equality operators. Should use === instead of == for type-safe comparison.

Copilot uses AI. Check for mistakes.
throw new Error('orgName is required');
}
if (type == 'enterprise' && !enterpriseName) {
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

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

Use strict equality operators. Should use === instead of == for type-safe comparison.

Suggested change
if (type == 'enterprise' && !enterpriseName) {
if (type === 'enterprise' && !enterpriseName) {

Copilot uses AI. Check for mistakes.
throw new Error('enterpriseName is required');
}

// stop processing after specified date
const sinceDate = sinceIso ? new Date(sinceIso) : undefined;

// Build the actor phrase to filter by users
const actorPhrase = usernames
? Array.from(usernames)
.map((username) => `actor:${username}`)
.join(' ')
: '';

// Build the action phrase to filter by action
const actionPhrase = action
? Array.isArray(action)
? action.map((a) => `action:${a}`).join(' ')
: `action:${action}`
: '';

// Build the created phrase to filter by date
const createdPhrase = created
? Array.isArray(created)
? created.map((c) => `created:${c}`).join(' ')
: `created:${created}`
: '';

const repoPhrase = repoName ? `repo:${orgName}/${repoName}` : '';
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

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

Potential bug when repoName is provided without orgName. The repoPhrase construction assumes orgName is available when repoName is provided (line 78), but orgName is optional. This could result in an invalid phrase like repo:undefined/repoName if repoName is provided but orgName is not.

Suggested change
const repoPhrase = repoName ? `repo:${orgName}/${repoName}` : '';
const repoPhrase = repoName && orgName ? `repo:${orgName}/${repoName}` : '';

Copilot uses AI. Check for mistakes.

const orgPhrase = orgName ? `org:${orgName}` : '';

const actionPhrases = [
actionPhrase,
orgPhrase,
repoPhrase,
actorPhrase,
createdPhrase,
].filter((phrase) => phrase.trim().length > 0);

// Build the full search phrase
const phrase = actionPhrases.join(' ').trim();

const url =
type == 'org'
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

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

Use strict equality operators. Should use === instead of == for type-safe comparison.

Suggested change
type == 'org'
type === 'org'

Copilot uses AI. Check for mistakes.
? 'GET /orgs/{org}/audit-log'
: 'GET /enterprises/{enterprise}/audit-log';

// use pagination to fetch all audit log events matching the criteria
const iterator = octokit.paginate.iterator(url, {
org: orgName,
enterprise: enterpriseName,
phrase: phrase,
order: 'desc',
include: include,
per_page: 30,
});

let continueFetching = true;
for await (const { data: events } of iterator) {
for (const event of events) {
const eventDate = new Date((event as any)['@timestamp']);

if (sinceDate && eventDate < sinceDate) {
continueFetching = false;
break;
}

const data: any = event as any;
yield {
actor: data.actor,
action: data.action,
date: eventDate,
props: { ...(event as any) },
};
}
if (!continueFetching) {
break;
}
}
}
6 changes: 6 additions & 0 deletions src/commands/command-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export function withSharedOptions(cmd: Command): Command {
'The name of the organization to process',
).env('ORG_NAME'),
)
.addOption(
new Option(
'-e, --enterprise-name <enterprise>',
'The name of the enterprise to process',
).env('ENTERPRISE_NAME'),
)
.addOption(
new Option('-t, --access-token <token>', 'GitHub access token').env(
'ACCESS_TOKEN',
Expand Down
Loading
Loading