Created
February 26, 2025 04:44
-
-
Save naoyashiga/afdd55e37bc1f5e3376487d2d9ff191e to your computer and use it in GitHub Desktop.
GitHub Organization 内にあるパブリックリポジトリと同名のリポジトリが新たに作られていないかを自動チェック
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: "Diff Display and Update Cache" | |
description: "Compare current top5 results with previous cache, display new additions, and update the cache file." | |
inputs: | |
currentResults: | |
description: "The JSON string of current top5 results from malware-check action." | |
required: true | |
orgName: | |
description: "The GitHub organization name." | |
required: true | |
runs: | |
using: "composite" | |
steps: | |
- name: Compare current results with previous cache and update cache | |
id: diff-update | |
uses: actions/github-script@v7 | |
env: | |
INPUT_ORGNAME: ${{ inputs.orgName }} | |
with: | |
script: | | |
const GREEN = "\x1b[32m"; | |
const YELLOW = "\x1b[33m"; | |
const CYAN = "\x1b[36m"; | |
const RESET = "\x1b[0m"; | |
const owner = context.repo.owner; | |
const repo = context.repo.repo; | |
// currentResults を入力から取得 | |
const currentResults = JSON.parse(core.getInput('currentResults') || "{}"); | |
let previousResults = {}; | |
let fileSha = null; | |
try { | |
const fileResponse = await github.rest.repos.getContent({ | |
owner, | |
repo, | |
path: 'top5_cache.json', | |
ref: 'main' | |
}); | |
const content = Buffer.from(fileResponse.data.content, 'base64').toString('utf8'); | |
previousResults = JSON.parse(content); | |
fileSha = fileResponse.data.sha; | |
} catch (error) { | |
if (error.status === 404) { | |
core.info("No previous cache found. Starting fresh."); | |
previousResults = {}; | |
} else { | |
throw error; | |
} | |
} | |
// 差分算出: 各対象リポジトリごとに、current にあって previous にない項目(新規追加)のみ抽出 | |
const differences = {}; | |
for (const repoName in currentResults) { | |
const currList = currentResults[repoName] || []; | |
const prevList = previousResults[repoName] || []; | |
const prevNames = new Set(prevList.map(item => item.full_name)); | |
const newItems = currList.filter(item => !prevNames.has(item.full_name)); | |
if (newItems.length > 0) { | |
differences[repoName] = newItems; | |
} | |
} | |
if (Object.keys(differences).length === 0) { | |
core.info(`${YELLOW}No new additions detected compared to previous state.${RESET}`); | |
} else { | |
for (const repoName in differences) { | |
core.info(`\nFor organization repo: ${repoName}`); | |
differences[repoName].forEach(item => { | |
core.info(`${GREEN}New addition: ${item.full_name}${RESET} | ${CYAN}Created: ${item.created_at} | Stars: ${item.stargazers_count} | URL: ${item.html_url}`); | |
}); | |
} | |
} | |
// キャッシュの更新: currentResults を top5_cache.json として保存 | |
const newContent = Buffer.from(JSON.stringify(currentResults, null, 2)).toString('base64'); | |
const commitMessage = `Update top5 cache: ${new Date().toISOString()}`; | |
await github.rest.repos.createOrUpdateFileContents({ | |
owner, | |
repo, | |
path: 'top5_cache.json', | |
message: commitMessage, | |
content: newContent, | |
sha: fileSha || undefined | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: "Malware Repository Checker" | |
description: "Search same-named repositories and output current top5 results per repo." | |
inputs: | |
orgName: | |
description: "The GitHub organization name to check." | |
required: true | |
outputs: | |
currentResults: | |
description: "The JSON string representing the current top5 results for each repo." | |
runs: | |
using: "composite" | |
steps: | |
# ----------------------------------------------------------- | |
# 1. 対象組織の public リポジトリを、直近1年以内かつ非 archived でフィルタし、更新日時降順の上位20件を取得 | |
# ----------------------------------------------------------- | |
- name: Get target repos from organization | |
id: get-repos | |
uses: actions/github-script@v7 | |
env: | |
INPUT_ORGNAME: ${{ inputs.orgName }} | |
with: | |
script: | | |
const orgName = core.getInput('orgName'); | |
core.info(`Searching repos for organization: ${orgName}`); | |
// 最大50件取得してからフィルタする | |
const { data } = await github.rest.repos.listForOrg({ | |
org: orgName, | |
type: 'public', | |
sort: 'updated', | |
direction: 'desc', | |
per_page: 50 | |
}); | |
const oneYearAgo = new Date(); | |
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); | |
const filteredRepos = data.filter(r => new Date(r.updated_at) >= oneYearAgo && !r.archived); | |
const topRepos = filteredRepos.slice(0, 20); | |
const repoNames = topRepos.map(r => r.name); | |
core.info(`Target repos: ${repoNames.join(', ')}`); | |
core.setOutput("repoNames", JSON.stringify(repoNames)); | |
# ----------------------------------------------------------- | |
# 2. 各リポジトリ名ごとに、GitHub全体から同名(完全一致、大文字小文字無視、かつ対象組織外)の上位5件を検索 | |
# ----------------------------------------------------------- | |
- name: Search for same-named repositories | |
id: search-repos | |
uses: actions/github-script@v6 | |
env: | |
INPUT_ORGNAME: ${{ inputs.orgName }} | |
REPO_NAMES: ${{ steps.get-repos.outputs.repoNames }} | |
with: | |
script: | | |
// ANSIカラーコード(ログ用) | |
const GREEN = "\x1b[32m"; | |
const YELLOW = "\x1b[33m"; | |
const CYAN = "\x1b[36m"; | |
const RESET = "\x1b[0m"; | |
const orgName = process.env.INPUT_ORGNAME; | |
const repoNames = JSON.parse(process.env.REPO_NAMES); | |
const currentResults = {}; | |
for (const repoName of repoNames) { | |
core.info(`\n[Checking repo: ${repoName}] ------------------------`); | |
// 検索: repoName in:name, 作成日降順、最大50件取得 | |
const searchResponse = await github.rest.search.repos({ | |
q: `${repoName} in:name`, | |
sort: 'created', | |
order: 'desc', | |
per_page: 50, | |
}); | |
// 完全一致かつ対象組織外のみ抽出 | |
const matched = searchResponse.data.items.filter(item => | |
item.name.toLowerCase() === repoName.toLowerCase() && | |
item.owner.login.toLowerCase() !== orgName.toLowerCase() | |
); | |
const top5 = matched.slice(0, 5); | |
if (top5.length === 0) { | |
core.info(`${YELLOW}No same-named repos found for ${repoName}.${RESET}`); | |
} else { | |
top5.forEach(repo => { | |
core.info(`${GREEN}Found: ${repo.full_name}${RESET} | ${CYAN}Created: ${repo.created_at} | Stars: ${repo.stargazers_count} | URL: ${repo.html_url}`); | |
}); | |
} | |
currentResults[repoName] = top5; | |
} | |
// 出力として currentResults をセット | |
core.setOutput("currentResults", JSON.stringify(currentResults)); | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Malware Check | |
on: | |
pull_request: | |
branches: | |
- main | |
types: [opened, synchronize] | |
push: | |
branches: | |
- main | |
# workflow_dispatch: # 手動実行する場合はコメントアウトを外す | |
permissions: | |
contents: write | |
jobs: | |
malware-check-job: | |
runs-on: ubuntu-latest | |
steps: | |
- name: Check out repository | |
uses: actions/checkout@v4 | |
- name: Setup Node.js | |
uses: actions/setup-node@v4 | |
with: | |
node-version: '20.x' | |
- name: Run Malware Check Action | |
id: malware-check | |
uses: ./.github/actions/malware-check | |
with: | |
orgName: layerXcom | |
- name: Run Diff Display Action | |
uses: ./.github/actions/diff-display | |
with: | |
orgName: layerXcom | |
currentResults: ${{ steps.malware-check.outputs.currentResults }} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
README
概要
このリポジトリでは、GitHub Organization 内にあるパブリックリポジトリと同名のリポジトリが新たに作られていないかを自動チェックし、前回の結果との差分を検知する仕組みを提供します。
本仕組みは、2つのアクションを通じて実行されます。
malware-check アクション
diff-display アクション
top5_cache.json
)と、今回の結果を比較し、新規に追加されたリポジトリだけを検知してログ出力します。この仕組みにより、悪意あるコピー・マルウェアを仕込んだリポジトリの早期発見を支援します。