aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKonstantin Ryabitsev <konstantin@linuxfoundation.org>2020-10-23 16:35:06 -0400
committerKonstantin Ryabitsev <konstantin@linuxfoundation.org>2020-10-23 16:35:06 -0400
commit55ce7342964fb2be2a6d61491eaaacc41bb35b9c (patch)
tree87ed7ed61aacba3129b6599a0524db73c98a7691
parent35fa657cf9ee91aea540317b823fb3485fad0bd3 (diff)
downloadkorg-helpers-55ce7342964fb2be2a6d61491eaaacc41bb35b9c.tar.gz
Add post-receive-activity-feed
This is the hook that we are rolling out to generate the activity feed suitable for historical (and forensic, should it ever be necessary) analysis. Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
-rwxr-xr-xpost-receive-activity-feed165
1 files changed, 165 insertions, 0 deletions
diff --git a/post-receive-activity-feed b/post-receive-activity-feed
new file mode 100755
index 0000000..5a1ec77
--- /dev/null
+++ b/post-receive-activity-feed
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# This is the post-receive git hook used to generate the activity feed
+# public-inbox repository. It requires that ezpi is installed
+# https://sr.ht/~monsieuricon/ezpi/
+#
+# Copyright (C) 2020 by The Linux Foundation
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+__author__ = 'Konstantin Ryabitsev <konstantin@linuxfoundation.org>'
+
+import os
+import sys
+import ezpi
+import hashlib
+import base64
+
+from email.message import EmailMessage
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+
+from typing import Optional
+
+
+def get_config_from_git(regexp: str, defaults: Optional[dict] = None) -> dict:
+ gitconfig = defaults if defaults else dict()
+
+ args = ['config', '-z', '--get-regexp', regexp]
+ ee, out, err = ezpi.git_run_command('', args)
+ if ee > 0 or not len(out):
+ return gitconfig
+
+ for line in out.decode().split('\x00'):
+ if not line:
+ continue
+ key, value = line.split('\n', 1)
+ try:
+ chunks = key.split('.')
+ cfgkey = chunks[-1]
+ gitconfig[cfgkey.lower()] = value
+ except ValueError:
+ pass
+
+ return gitconfig
+
+
+def run_hook(feedrepo: str, fromhdr: str, domain: str):
+ # Look if we have a GL_USER and GL_REPO in the env
+ user = os.getenv('GL_USER')
+ if not user:
+ user = os.getenv('USER')
+ repo = os.getenv('GL_REPO')
+ if not repo:
+ repo = os.getcwd()
+ ll = list()
+ attachments = list()
+ ll.append('---')
+ ll.append('service: git-receive-pack')
+ ll.append(f'repo: {repo}')
+ ll.append(f'user: {user}')
+ # Do we have a ~/.activity-feed-secret?
+ secret = None
+ secretf = os.path.expanduser('~/.activity-feed-secret')
+ # The idea is to rotate it frequently, with the value logged in syslog.
+ # This allows us to see if a push is coming from the same remote IP address,
+ # but only within the same calendar day.
+ try:
+ with open(secretf) as fh:
+ secret = fh.read().strip()
+ except (FileNotFoundError, IOError):
+ pass
+
+ if secret:
+ conn_info = os.getenv('SSH_CONNECTION')
+ if conn_info:
+ remote_ip = conn_info.split()[0]
+ ipline = f'{secret}{user}{remote_ip}'
+ iph = hashlib.sha1()
+ iph.update(ipline.encode())
+ hashed = base64.b64encode(iph.digest()).decode()
+ ll.append(f'remote_ip: {hashed}')
+
+ # Do we have a push cert?
+ cert = os.getenv('GIT_PUSH_CERT')
+ if cert:
+ gpcstatus = os.getenv('GIT_PUSH_CERT_STATUS')
+ ll.append(f'git_push_cert_status: {gpcstatus}')
+ args = ['cat-file', 'blob', cert]
+ ee, out, err = ezpi.git_run_command('', args)
+ if ee == 0 and out:
+ attachments.append(('git-push-certificate.txt', out.decode()))
+
+ ll.append('changes:')
+ while True:
+ line = sys.stdin.readline()
+ if not line:
+ break
+ oldrev, newrev, ref = line.strip().split()
+ ll.append(f' - ref: {ref}')
+ ll.append(f' old: {oldrev}')
+ ll.append(f' new: {newrev}')
+
+ args = ['rev-list', '--max-count=1024', '--reverse', '--pretty=oneline', newrev]
+ if set(oldrev) != {0}:
+ args += [f'^{oldrev}']
+
+ ee, out, err = ezpi.git_run_command('', args)
+ if ee > 0 or not len(out):
+ continue
+
+ if len(out) > 1024:
+ # Add it as attachment
+ filename = f'revlist-{oldrev[:12]}-{newrev[:12]}.txt'
+ attachments.append((filename, out.decode()))
+ ll.append(f' log: {filename}')
+ continue
+
+ ll.append(' log: |')
+ for pretty in out.decode().split('\n'):
+ ll.append(f' {pretty}')
+
+ body = '\n'.join(ll) + '\n'
+
+ if attachments:
+ msg = MIMEMultipart()
+ msg.attach(MIMEText(body, 'plain'))
+ for attfilename, attbody in attachments:
+ att = MIMEText(attbody, 'plain')
+ att.add_header('Content-Disposition', f'attachment; filename={attfilename}')
+ msg.attach(att)
+ else:
+ msg = EmailMessage()
+ msg.set_payload(body)
+
+ msg['From'] = fromhdr
+ msg['Subject'] = f'post-receive: {repo}'
+
+ try:
+ ezpi.add_rfc822(feedrepo, msg, domain)
+ sys.stderr.write('Recorded in the activity feed\n')
+ ezpi.run_hook(feedrepo)
+ except RuntimeError:
+ # Could not add it to the feed, complain
+ sys.stderr.write('FAILED writing to the activity feed!\n')
+
+
+if __name__ == '__main__':
+ if sys.stdin.isatty():
+ # Nothing passed via stdin, so nothing to add to the feed
+ sys.exit(0)
+ config = get_config_from_git(r'activityfeed\..*')
+ _feedrepo = config.get('repo')
+ if not config.get('repo'):
+ # The audit repo is not defined in gitconfig, so nothing for us to do.
+ sys.exit(0)
+ _fromhdr = config.get('from')
+ _domain = config.get('domain')
+ if not _domain:
+ _domain = 'localhost'
+
+ if not _fromhdr:
+ _fromhdr = f'Post-Receive Hook <post-receive@{_domain}>'
+
+ run_hook(_feedrepo, _fromhdr, _domain)