Skip to content

Commit 58b9023

Browse files
committed
* Added a brand new HTML output format/report by default, making results a lot easier to navigate! Custom search bar instead of relying on DataTables which can be super slow for large HTML files. We're now also groupping results by Category across all contributors and highlighting results which contain a WARNING keyword.
* Added certain association results to Contributor results, not all to prevent extra noise. * Added the ability to specify a directory for output instead of a file, gitxray creating the filename for you. * Removed the concept of 'Verbose' results, merging them with the non-verbose categories. * Removed the need for repositories and organizations to start with https://github.com (Thanks to @mattaereal for pointing that out!)
1 parent 18d7c4b commit 58b9023

File tree

9 files changed

+328
-114
lines changed

9 files changed

+328
-114
lines changed

src/gitxray/gitxray.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def gitxray_cli():
2020
░░██████ ░░██████
2121
░░░░░░ ░░░░░░
2222
gitxray: X-Ray and analyze Github Repositories and their Contributors. Trust no one!
23-
v1.0.15 - Developed by Kulkan Security [www.kulkan.com] - Penetration testing by creative minds.
23+
v1.0.16 - Developed by Kulkan Security [www.kulkan.com] - Penetration testing by creative minds.
2424
"""+"#"*gx_definitions.SCREEN_SEPARATOR_LENGTH)
2525

2626
# Let's initialize a Gitxray context, which parses arguments and more.
@@ -37,8 +37,13 @@ def gitxray_cli():
3737
else:
3838
gx_output.notify(f"GitHub Token loaded from {gx_definitions.ENV_GITHUB_TOKEN} env variable.")
3939

40-
if not gx_context.verboseEnabled():
41-
gx_output.notify(f"Verbose mode is DISABLED. You might want to use -v if you're hungry for information.")
40+
gx_output.notify(f"Output format set to [{gx_context.getOutputFormat().upper()}] - You may change it with -outformat.")
41+
42+
if gx_context.getOutputFile():
43+
gx_output.notify(f"Output file set to: {str(gx_context.getOutputFile())} - You may change it with -out.")
44+
if gx_context.getOrganizationTarget():
45+
# Let's warn the user that in Organization mode, the output file will contain a repository name preffix
46+
gx_output.warn("The Output file name when targetting an Organization will include an Organization and Repository prefix.")
4247

4348
if gx_context.getOutputFilters():
4449
gx_output.notify(f"You have ENABLED filters - You will only see results containing the following keywords: {str(gx_context.getOutputFilters())}")
@@ -76,6 +81,9 @@ def gitxray_cli():
7681
# Let's keep track of the repository that we're X-Raying
7782
gx_context.setRepository(repository)
7883

84+
# if an Organization is in target, add a repository prefix to the output filename
85+
if gx_context.getOrganizationTarget() and gx_context.getOutputFile(): gx_context.setOutputFilePrefix(repository.get("full_name"))
86+
7987
# Now call our xray modules! Specifically by name, until we make this more plug and play
8088
# The standard is that a return value of False leads to skipping additional modules
8189

src/gitxray/include/gh_public_events.py

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,80 +33,77 @@ def log_events(events, gx_output, for_repository=False):
3333

3434
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#commitcommentevent
3535
if etype == "CommitCommentEvent":
36-
logging_func(f"{ts}: {actor}created a comment in a commit: [{payload.get('comment').get('html_url')}]", rtype="v_90d_events")
36+
logging_func(f"{ts}: {actor}created a comment in a commit: [{payload.get('comment').get('html_url')}]", rtype="90d_events")
3737
pass
3838

3939
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#createeventA
4040
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#deleteevent
4141
elif etype == "CreateEvent" or etype == "DeleteEvent":
4242
action = "created" if etype == "CreateEvent" else "deleted"
4343
if payload.get('ref_type') == "repository":
44-
logging_func(f"{ts}: {actor}{action} a repository: [{event.get('repo').get('name')}]", rtype="v_90d_events")
44+
logging_func(f"{ts}: {actor}{action} a repository: [{event.get('repo').get('name')}]", rtype="90d_events")
4545
else:
46-
logging_func(f"{ts}: {actor}{action} a {payload.get('ref_type')}: [{payload.get('ref')}] in repo [{event.get('repo').get('name')}]", rtype="v_90d_events")
46+
logging_func(f"{ts}: {actor}{action} a {payload.get('ref_type')}: [{payload.get('ref')}] in repo [{event.get('repo').get('name')}]", rtype="90d_events")
4747

4848
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#forkevent
4949
elif etype == "ForkEvent":
50-
logging_func(f"{ts}: {actor}forked a repository: {event.get('repo').get('name')} into {payload.get('forkee').get('full_name')}", rtype="v_90d_events")
50+
logging_func(f"{ts}: {actor}forked a repository: {event.get('repo').get('name')} into {payload.get('forkee').get('full_name')}", rtype="90d_events")
5151

5252
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#gollumevent
5353
elif etype == "GollumEvent":
5454
for page in payload.get('pages'):
55-
logging_func(f"{ts}: {actor}{page.get('action')} Wiki page at [{page.get('html_url')}]", rtype="v_90d_events")
55+
logging_func(f"{ts}: {actor}{page.get('action')} Wiki page at [{page.get('html_url')}]", rtype="90d_events")
5656

5757
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#issuecommentevent
5858
elif etype == "IssueCommentEvent":
59-
logging_func(f"{ts}: {actor}{action} a comment in an Issue [{payload.get('issue').get('html_url')}]", rtype="v_90d_events")
59+
logging_func(f"{ts}: {actor}{action} a comment in an Issue [{payload.get('issue').get('html_url')}]", rtype="90d_events")
6060

6161
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#issuesevent
6262
elif etype == "IssuesEvent":
63-
logging_func(f"{ts}: {actor}{action} an Issue: [{payload.get('issue').get('html_url')}]", rtype="v_90d_events")
63+
logging_func(f"{ts}: {actor}{action} an Issue: [{payload.get('issue').get('html_url')}]", rtype="90d_events")
6464

6565
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#memberevent
6666
elif etype == "MemberEvent":
6767
added_who = payload.get('member').get('login')
6868
to_repo = event.get('repo').get('name')
69-
logging_func(f"{ts}: {actor}{action} a user [{added_who}] as a collaborator to repo: [{to_repo}]", rtype="v_90d_events")
69+
logging_func(f"{ts}: {actor}{action} a user [{added_who}] as a collaborator to repo: [{to_repo}]", rtype="90d_events")
7070

7171
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#publicevent
7272
elif etype == "PublicEvent":
73-
logging_func(f"{ts}: {actor}switched a repository from PRIVATE to PUBLIC, repo: [{event.get('repo').get('name')}]", rtype="v_90d_events")
73+
logging_func(f"{ts}: {actor}switched a repository from PRIVATE to PUBLIC, repo: [{event.get('repo').get('name')}]", rtype="90d_events")
7474

7575
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#pullrequestevent
7676
elif etype == "PullRequestEvent":
77-
logging_func(f"{ts}: {actor}{action} a PR: [{payload.get('pull_request').get('html_url')}]", rtype="v_90d_events")
77+
logging_func(f"{ts}: {actor}{action} a PR: [{payload.get('pull_request').get('html_url')}]", rtype="90d_events")
7878

7979
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#pullrequestreviewevent
8080
elif etype == "PullRequestReviewEvent":
81-
logging_func(f"{ts}: {actor}{action} a PR Review: [{payload.get('pull_request').get('html_url')}]", rtype="v_90d_events")
81+
logging_func(f"{ts}: {actor}{action} a PR Review: [{payload.get('pull_request').get('html_url')}]", rtype="90d_events")
8282

8383
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#pullrequestreviewcommentevent
8484
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#pullrequestreviewthreadevent
8585
elif etype == "PullRequestReviewCommentEvent" or etype == "PullRequestReviewThreadEvent":
86-
logging_func(f"{ts}: {actor}{action} a comment in PR: [{payload.get('pull_request').get('html_url')}]", rtype="v_90d_events")
86+
logging_func(f"{ts}: {actor}{action} a comment in PR: [{payload.get('pull_request').get('html_url')}]", rtype="90d_events")
8787

8888
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#pushevent
8989
elif etype == "PushEvent":
90-
logging_func(f"{ts}: {actor}pushed a total of {len(payload.get('commits'))} commits from: [{payload.get('ref')}]", rtype="v_90d_events")
90+
logging_func(f"{ts}: {actor}pushed a total of {len(payload.get('commits'))} commits from: [{payload.get('ref')}]", rtype="90d_events")
9191

9292
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#releaseevent
9393
elif etype == "ReleaseEvent":
94-
logging_func(f"{ts}: {actor}published a Release at [{payload.get('release').get('html_url')}]", rtype="v_90d_events")
94+
logging_func(f"{ts}: {actor}published a Release at [{payload.get('release').get('html_url')}]", rtype="90d_events")
9595

9696
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#sponsorshipevent
9797
elif etype == "SponsorshipEvent":
98-
logging_func(f"{ts}: {actor}{action} a Sponsorship Event]", rtype="v_90d_events")
98+
logging_func(f"{ts}: {actor}{action} a Sponsorship Event]", rtype="90d_events")
9999

100100
# https://docs.github.com/en/rest/using-the-rest-api/github-event-types?apiVersion=2022-11-28#watchevent
101101
elif etype == "WatchEvent":
102-
logging_func(f"{ts}: {actor}starred a repository: [{event.get('repo').get('name')}]", rtype="v_90d_events")
102+
logging_func(f"{ts}: {actor}starred a repository: [{event.get('repo').get('name')}]", rtype="90d_events")
103103
else:
104104
logging_func(f"Missing parser in recent events for: {etype} with {payload}", rtype="debug")
105105

106106

107-
# If verbose is enabled, we skip the summary as it becomes redundant.
108-
if gx_output.verbose_enabled(): return
109-
110107
# Now let's create the non-debug summarized version of messages.
111108
for (month, etype, action), count in event_summary.items():
112109
summary_message = f"In {month}, "
@@ -196,5 +193,4 @@ def log_events(events, gx_output, for_repository=False):
196193

197194

198195
logging_func(summary_message, rtype="90d_events")
199-
logging_func("For a detailed individual list of recent public events, use --verbose", rtype="90d_events")
200196
return

src/gitxray/include/gx_arg_parser.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Our argparser is called by the Gitxray Context, we don't talk to it directly.
2-
import os, sys, argparse, re
2+
import os, sys, argparse, re, datetime
33

44
def parse_repositories_from_file(filepath):
55
if not os.path.exists(filepath):
@@ -16,7 +16,7 @@ def parse_repositories_from_file(filepath):
1616

1717
def validate_repository_org_link(repo):
1818
if not repo.startswith("https://"):
19-
raise argparse.ArgumentTypeError(f"Invalid URL '{repo}'. It should start with 'https://'.")
19+
return f'https://github.com/{repo}'
2020
return repo
2121

2222
def validate_contributors(username_string):
@@ -39,7 +39,7 @@ def parse_arguments():
3939
group.add_argument('-r', '--repository',
4040
type=validate_repository_org_link,
4141
action='store',
42-
help='The repository to check (an https:// URL)')
42+
help='The repository to check (Including https://github.com/ is optional)')
4343

4444
group.add_argument('-rf', '--repositories-file',
4545
type=parse_repositories_from_file,
@@ -49,7 +49,7 @@ def parse_arguments():
4949
group.add_argument('-o', '--organization',
5050
type=validate_repository_org_link,
5151
action='store',
52-
help='An organization to check all of their repositories (an https:// URL)')
52+
help='An organization to check all of their repositories (Including https://github.com/ is optional)')
5353

5454
group_two = parser.add_mutually_exclusive_group(required=False)
5555

@@ -68,11 +68,6 @@ def parse_arguments():
6868
action='store',
6969
help="Comma separated keywords to filter results by (e.g. private,macbook).")
7070

71-
parser.add_argument('-v', '--verbose',
72-
action='store_true',
73-
default=False,
74-
help='Verbose output. For example, print a detailed list of public events instead of a summary.')
75-
7671
parser.add_argument('--debug',
7772
action='store_true',
7873
default=False,
@@ -84,16 +79,20 @@ def parse_arguments():
8479
help='Set the location for the output log file.')
8580

8681
parser.add_argument('-outformat', '--output-format', type=str, action='store',
87-
default='text',
88-
help='Format for log file (text,json) - default: text',
89-
choices = ['text', 'json'])
82+
default='html',
83+
help='Format for log file (html,text,json) - default: html',
84+
choices = ['html', 'text', 'json'])
9085

9186
args = parser.parse_args()
9287

88+
# If output format is 'html' and outfile is not specified, set it to current date and time
89+
current_datetime = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
90+
if args.output_format == 'html' and not args.outfile:
91+
args.outfile = f"gitxray_{current_datetime}.html"
92+
9393
if args.outfile:
9494
if os.path.isdir(args.outfile):
95-
print("[!] Can't specify a directory as the output file, exiting.")
96-
sys.exit()
95+
args.outfile = f"{args.outfile}gitxray_{current_datetime}.html"
9796
if os.path.isfile(args.outfile):
9897
target = args.outfile
9998
else:

src/gitxray/include/gx_context.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from . import gx_arg_parser, gx_definitions
22
from collections import defaultdict
3-
import os
3+
import os, re
44

55
class Context:
66
def __init__(self):
@@ -10,27 +10,34 @@ def __init__(self):
1010

1111
def reset(self):
1212
self._identifier_user_relationship = defaultdict(list)
13+
self._outfile_prefix = None
1314

1415
def usingToken(self):
1516
return self._USING_TOKEN != None
1617

1718
def debugEnabled(self):
1819
return self._cmd_args.debug
1920

20-
def verboseEnabled(self):
21-
return self._cmd_args.verbose
22-
23-
def verboseLegend(self):
24-
return "" if self.verboseEnabled() else "Set verbose mode for more data. "
25-
2621
def listAndQuit(self):
2722
return self._cmd_args.list
2823

2924
def getOutputFile(self):
30-
return self._cmd_args.outfile
25+
outfile = self._cmd_args.outfile
26+
if self._outfile_prefix:
27+
directory, filename = os.path.split(outfile)
28+
slug = re.sub(r'[^A-Za-z0-9_]', '_', self._outfile_prefix)
29+
slug = re.sub(r'_+', '_', slug).strip('_')
30+
prefixed_filename = f'{slug}_{filename}'
31+
return os.path.join(directory, prefixed_filename)
32+
33+
return outfile
34+
35+
def setOutputFilePrefix(self, prefix):
36+
self._outfile_prefix = prefix
37+
return
3138

3239
def getOutputFormat(self):
33-
return self._cmd_args.output_format if self._cmd_args.output_format is not None else "text"
40+
return self._cmd_args.output_format if self._cmd_args.output_format is not None else "html"
3441

3542
def getOutputFilters(self):
3643
return self._cmd_args.filters

0 commit comments

Comments
 (0)