diff --git a/.github/actions/check/action.yml b/.github/actions/check/action.yml new file mode 100644 index 0000000..941ff80 --- /dev/null +++ b/.github/actions/check/action.yml @@ -0,0 +1,10 @@ +name: 'check-replication-action' +description: 'Checks that all external bounties are replicated internally' +author: 'xcorail' +inputs: + internal_repo: + description: 'The destination repo for the internal issue' + default: 'github/securitylab-bounties' +runs: + using: 'node12' + main: './check-replication.js' \ No newline at end of file diff --git a/.github/actions/check/check-replication.js b/.github/actions/check/check-replication.js new file mode 100644 index 0000000..b8b9a8a --- /dev/null +++ b/.github/actions/check/check-replication.js @@ -0,0 +1,55 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const github = __importStar(require("@actions/github")); +const issues_1 = require("../replicate/issues"); +const run = async () => { + const internalRepoAccessToken = process.env['INT_REPO_TOKEN']; + const internalRepo = core.getInput('internal_repo') || '/'; + const [owner, repo] = internalRepo.split('/'); + const internalIssues = await issues_1.getIssueList(owner, repo, internalRepoAccessToken, false, false); + if (!internalIssues) { + core.setFailed(`Internal error. Cannot access the internal repo ${internalRepo}. Aborting`); + return; + } + else { + const externalIssues = await issues_1.getIssueList(github.context.repo.owner, github.context.repo.repo, process.env['GITHUB_TOKEN'], true, true); + if (!externalIssues) { + core.setFailed(`Internal error when retrieving all issues.`); + return; + } + let failed = false; + externalIssues.forEach(issue => { + const ref = issues_1.internalIssueAlreadyCreated(issue === null || issue === void 0 ? void 0 : issue.html_url, internalIssues); + if (!ref) { + core.debug(`External issue ${issue === null || issue === void 0 ? void 0 : issue.number} is not replicated internally.`); + failed = true; + } + }); + if (failed) { + core.setFailed("Some submissions are not replicated internally, see execution logs."); + } + } + return; +}; +run(); +//# sourceMappingURL=check-replication.js.map \ No newline at end of file diff --git a/.github/actions/check/check-replication.ts b/.github/actions/check/check-replication.ts new file mode 100644 index 0000000..dec22e8 --- /dev/null +++ b/.github/actions/check/check-replication.ts @@ -0,0 +1,35 @@ +import * as core from '@actions/core' +import * as github from '@actions/github' +import { getIssueList, internalIssueAlreadyCreated } from '../replicate/issues' + +const run = async (): Promise => { + const internalRepoAccessToken: string | undefined = process.env['INT_REPO_TOKEN'] + const internalRepo = core.getInput('internal_repo') || '/' + const [owner, repo] = internalRepo.split('/') + const internalIssues = await getIssueList(owner, repo, internalRepoAccessToken, false, false) + if(!internalIssues) { + core.setFailed(`Internal error. Cannot access the internal repo ${internalRepo}. Aborting`) + return + } else { + const externalIssues = await getIssueList(github.context.repo.owner, github.context.repo.repo, process.env['GITHUB_TOKEN'], true, true) + if(!externalIssues) { + core.setFailed(`Internal error when retrieving all issues.`) + return + } + let failed = false + externalIssues.forEach( issue => { + const ref = internalIssueAlreadyCreated(issue?.html_url, internalIssues) + if(!ref) { + core.debug(`External issue ${issue?.number} is not replicated internally.`) + failed = true + } + }) + if(failed) { + core.setFailed("Some submissions are not replicated internally, see execution logs.") + } + } + return +} + +run() + diff --git a/.github/actions/replicate/action.yml b/.github/actions/replicate/action.yml index 771e8c5..8860a2b 100644 --- a/.github/actions/replicate/action.yml +++ b/.github/actions/replicate/action.yml @@ -1,5 +1,5 @@ -name: 'debug-action' -description: 'Outputs debug information' +name: 'replicate-action' +description: 'Replicates bounty internal' author: 'xcorail' inputs: internal_repo: @@ -8,6 +8,9 @@ inputs: existing_issue: description: 'Launching on existing issues: we check duplicates, and we do not comment the original issue' default: false + specific_issue: + description: 'Specific issue to replicate, in case of manual trigger' + default: '' runs: using: 'node12' main: './replicate.js' \ No newline at end of file diff --git a/.github/actions/replicate/issues.js b/.github/actions/replicate/issues.js index 9257f7b..4830fb4 100644 --- a/.github/actions/replicate/issues.js +++ b/.github/actions/replicate/issues.js @@ -50,7 +50,8 @@ exports.getIssueList = async (owner, repo, token, open, checkBountyLabels, per_p title: issue.title, author: (_a = issue.user) === null || _a === void 0 ? void 0 : _a.login, body: issue.body ? issue.body : '', - number: issue.number + number: issue.number, + html_url: issue.html_url }; result.push(item); } diff --git a/.github/actions/replicate/issues.ts b/.github/actions/replicate/issues.ts index 20f3a67..33945f0 100644 --- a/.github/actions/replicate/issues.ts +++ b/.github/actions/replicate/issues.ts @@ -2,7 +2,7 @@ import * as core from '@actions/core' import * as github from '@actions/github' import * as replicate from './replicate' -export type Issue_info = {title: string, author: string, body: string, number: number} +export type Issue_info = {title: string, author: string, body: string, number: number, html_url?: string} type Issue_state = 'open' | 'all' | 'closed' | undefined export const getIssueList = async (owner: string, repo: string, token: string | undefined, open: boolean, checkBountyLabels: boolean, per_page?: number) : Promise => { @@ -32,7 +32,8 @@ export const getIssueList = async (owner: string, repo: string, token: string | title: issue.title, author: issue.user?.login, body: issue.body? issue.body : '', - number: issue.number + number: issue.number, + html_url: issue.html_url } result.push(item) } diff --git a/.github/actions/replicate/replicate.js b/.github/actions/replicate/replicate.js index c306442..1fc4a4a 100644 --- a/.github/actions/replicate/replicate.js +++ b/.github/actions/replicate/replicate.js @@ -66,11 +66,24 @@ const COMMENT_SCORING = `## Scoring - [ ] Accept `; const COMMENT_FIRST_SUBMISSION = `## :tada: First submission for this user :tada:`; -exports.generateInternalIssueContentFromPayload = async (payload) => { - const issue = payload.issue; +const getIssueFromRef = async (issueRef) => { + if (!issueRef) + return undefined; + const token = process.env['GITHUB_TOKEN']; + if (token === undefined) + return undefined; + const octokit = new github.GitHub(token); + const issueResponse = await octokit.issues.get({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + issue_number: Number(issueRef), + }); + return issueResponse.data; +}; +exports.generateInternalIssueContentFromPayload = async (payload, issueRef) => { + const issue = await getIssueFromRef(issueRef) || (payload === null || payload === void 0 ? void 0 : payload.issue); let result = { title: 'none', body: 'none', labels: [], bountyType: 'All For One' }; let bountyIssue = false; - let bountyType = ''; if (!issue || !issue.user || !issue.html_url) { core.debug("Invalid issue payload"); return undefined; @@ -80,7 +93,7 @@ exports.generateInternalIssueContentFromPayload = async (payload) => { if (!bountyIssue) { bountyIssue = exports.BOUNTY_LABELS.includes(element.name); if (bountyIssue) { - bountyType = element.name; + result.bountyType = element.name; } } }); @@ -88,7 +101,7 @@ exports.generateInternalIssueContentFromPayload = async (payload) => { core.debug("Not a bounty"); return undefined; } - result.title = `[${bountyType}] ${issue.title}`, + result.title = `[${result.bountyType}] ${issue.title}`, // In order to differentiate immediately the issues from others in the repo // And with the current situation, the robot with Read access cannot add labels to the issue result.body = `Original external [issue](${issue.html_url}) @@ -203,7 +216,7 @@ exports.isFirstSubmission = async (payload, token) => { return !issues_1.isUserAlreadyParticipant((_a = payload.issue) === null || _a === void 0 ? void 0 : _a.user.login, allSubmissions); }; const run = async () => { - const internalIssue = await exports.generateInternalIssueContentFromPayload(github.context.payload); + const internalIssue = await exports.generateInternalIssueContentFromPayload(github.context.payload, core.getInput('specific_issue')); if (!internalIssue) return; const existingIssue = core.getInput('existingIssue') || true; diff --git a/.github/actions/replicate/replicate.ts b/.github/actions/replicate/replicate.ts index ab1c40c..c6e0f87 100644 --- a/.github/actions/replicate/replicate.ts +++ b/.github/actions/replicate/replicate.ts @@ -7,6 +7,7 @@ export const BOUNTY_LABELS = ['All For One', 'The Bug Slayer'] as const export type BountyType = typeof BOUNTY_LABELS[number] type CommentMap = {[K in BountyType]: string} export type Issue = {title: string, body: string, labels: string[], bountyType: BountyType} +type GitHubIssue = { [key: string]: any, number: number, html_url?: string | undefined, body?: string | undefined} const COMMENT_TASK_LIST_AFO = `## Task List - [ ] CodeQL Initial assessment - In case of rejection, please record your decision in the comment below: @@ -55,11 +56,25 @@ const COMMENT_SCORING = `## Scoring const COMMENT_FIRST_SUBMISSION = `## :tada: First submission for this user :tada:` -export const generateInternalIssueContentFromPayload = async (payload: WebhookPayload): Promise => { - const issue = payload.issue +const getIssueFromRef = async (issueRef: string | undefined): Promise => { + if(!issueRef) + return undefined + const token: string | undefined = process.env['GITHUB_TOKEN'] + if(token === undefined) + return undefined + const octokit: github.GitHub = new github.GitHub(token) + const issueResponse = await octokit.issues.get({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + issue_number: Number(issueRef), + }); + return issueResponse.data +} + +export const generateInternalIssueContentFromPayload = async (payload?: WebhookPayload, issueRef?: string): Promise => { + const issue = await getIssueFromRef(issueRef) || payload?.issue let result: Issue = {title: 'none', body: 'none', labels: [], bountyType: 'All For One'} let bountyIssue: boolean = false - let bountyType = '' if(!issue || !issue.user || !issue.html_url) { core.debug("Invalid issue payload") @@ -71,7 +86,7 @@ export const generateInternalIssueContentFromPayload = async (payload: WebhookPa if(!bountyIssue) { bountyIssue = BOUNTY_LABELS.includes(element.name) if(bountyIssue) { - bountyType = element.name + result.bountyType = element.name } } }); @@ -81,7 +96,7 @@ export const generateInternalIssueContentFromPayload = async (payload: WebhookPa return undefined } - result.title = `[${bountyType}] ${issue.title}`, + result.title = `[${result.bountyType}] ${issue.title}`, // In order to differentiate immediately the issues from others in the repo // And with the current situation, the robot with Read access cannot add labels to the issue result.body = `Original external [issue](${issue.html_url}) @@ -115,6 +130,13 @@ export const createInternalIssue = async (payload: WebhookPayload, issue: Issue) }) internal_ref = issueResponse.data.number core.debug(`issue created: ${internal_ref}`) + const labelsResponse = await octokit.issues.addLabels( { + owner, + repo, + issue_number: internal_ref, + labels: issue.labels + }) + core.debug(`Labels addition result: ${labelsResponse.status} ${(labelsResponse.status==200)? "OK" : "FAILED"}`) const issueCommentResponse1 = await octokit.issues.createComment({ owner, @@ -202,7 +224,7 @@ export const isFirstSubmission = async (payload: WebhookPayload, token : string } const run = async (): Promise => { - const internalIssue = await generateInternalIssueContentFromPayload(github.context.payload) + const internalIssue = await generateInternalIssueContentFromPayload(github.context.payload, core.getInput('specific_issue')) if(!internalIssue) return diff --git a/.github/workflows/check-replication-manual.yml b/.github/workflows/check-replication-manual.yml new file mode 100644 index 0000000..4892a36 --- /dev/null +++ b/.github/workflows/check-replication-manual.yml @@ -0,0 +1,19 @@ +name: 'Bounty issue replication workflow' +on: workflow_dispatch + +jobs: + build: + name: check-replicate-manual + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - run: npm install + - run: npm run build + - uses: ./.github/actions/check + with: + internal_repo: 'github/securitylab-bounties' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INT_REPO_TOKEN: ${{ secrets.INT_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/check-replication.yml b/.github/workflows/check-replication.yml new file mode 100644 index 0000000..3be6424 --- /dev/null +++ b/.github/workflows/check-replication.yml @@ -0,0 +1,21 @@ +name: 'Bounty issue replication workflow' +on: + schedule: + - cron: '0 17 * * *' + +jobs: + build: + name: check-replicate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - run: npm install + - run: npm run build + - uses: ./.github/actions/check + with: + internal_repo: 'github/securitylab-bounties' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INT_REPO_TOKEN: ${{ secrets.INT_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/replicate-manual.yml b/.github/workflows/replicate-manual.yml new file mode 100644 index 0000000..183ab15 --- /dev/null +++ b/.github/workflows/replicate-manual.yml @@ -0,0 +1,27 @@ +name: 'Bounty issue manual replication workflow' +on: + workflow_dispatch: + inputs: + issue: + description: 'Issue number to replicate' + required: true + +jobs: + build: + name: replicate-manual + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - run: npm install + - run: npm run build + - uses: ./.github/actions/replicate + with: + internal_repo: 'github/securitylab-bounties' + existing_issue: false + specific_issue: ${{ github.event.inputs.issue }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INT_REPO_TOKEN: ${{ secrets.INT_REPO_TOKEN }} + diff --git a/package-lock.json b/package-lock.json index 264002e..33daae0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3728,9 +3728,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash.get": {