Skip to content

Instantly share code, notes, and snippets.

@naoyashiga
Created February 26, 2025 04:44
Show Gist options
  • Save naoyashiga/afdd55e37bc1f5e3376487d2d9ff191e to your computer and use it in GitHub Desktop.
Save naoyashiga/afdd55e37bc1f5e3376487d2d9ff191e to your computer and use it in GitHub Desktop.
GitHub Organization 内にあるパブリックリポジトリと同名のリポジトリが新たに作られていないかを自動チェック
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
});
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));
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 }}
@naoyashiga
Copy link
Author

README

概要

このリポジトリでは、GitHub Organization 内にあるパブリックリポジトリ同名のリポジトリが新たに作られていないかを自動チェックし、前回の結果との差分を検知する仕組みを提供します。
本仕組みは、2つのアクションを通じて実行されます。

  1. malware-check アクション

    • 指定した GitHub Organization から、下記の条件でリポジトリを抽出します。
      • 直近1年以内に更新されている
      • アーカイブされていない
      • 上位20件
    • その上で、GitHub 全体から「同名のリポジトリ(ただし、同じOrganizationのものは除外)」を5件まで取得し、検索結果を JSON 形式にまとめて出力します。
  2. diff-display アクション

    • 前回の結果(キャッシュファイル top5_cache.json)と、今回の結果を比較し、新規に追加されたリポジトリだけを検知してログ出力します。
    • ログ出力後、最新の結果でキャッシュファイルを更新します。

この仕組みにより、悪意あるコピー・マルウェアを仕込んだリポジトリの早期発見を支援します。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment