Skip to content

Commit 983a62d

Browse files
committed
v0.1
1 parent 233b488 commit 983a62d

File tree

11 files changed

+429
-39
lines changed

11 files changed

+429
-39
lines changed

codeqldepgraph/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__name__ = "codeqldepgraph"
2+
__version__ = "0.1.0"
3+
4+
__url__ = "https://github.com/GeekMasher/codeql-dependency-graph-action"

codeqldepgraph/__main__.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,37 @@
1+
import os
2+
import json
13
import argparse
24

3-
import os
45
from codeqldepgraph.codeql import CodeQL, find_codeql, find_codeql_databases
6+
from codeqldepgraph.dependencies import parseDependencies, exportDependencies
7+
from codeqldepgraph.octokit import Octokit
58

69
parser = argparse.ArgumentParser(
710
description="Generate a dependency graph for a CodeQL database."
811
)
912

13+
parser.add_argument("-sha", default=os.environ.get("GITHUB_SHA"), help="Commit SHA")
14+
parser.add_argument("-ref", default=os.environ.get("GITHUB_REF"), help="Commit ref")
15+
1016
parser_github = parser.add_argument_group("GitHub")
11-
parser_github.add_argument("--github-token", help="GitHub API token")
12-
parser_github.add_argument("--github-repo", help="GitHub repository")
13-
parser_github.add_argument("--github-instance", help="GitHub instance")
17+
parser_github.add_argument(
18+
"-r",
19+
"--github-repo",
20+
default=os.environ.get("GITHUB_REPOSITORY"),
21+
help="GitHub repository",
22+
)
23+
parser_github.add_argument(
24+
"--github-instance",
25+
default=os.environ.get("GITHUB_SERVER_URL", "https://github.com"),
26+
help="GitHub instance",
27+
)
28+
parser_github.add_argument(
29+
"--github-token", default=os.environ.get("GITHUB_TOKEN"), help="GitHub API token"
30+
)
1431

1532
parser_codeql = parser.add_argument_group("CodeQL")
1633
parser_codeql.add_argument("--codeql-path", help="CodeQL executable")
34+
parser_codeql.add_argument("--codeql-pack", help="CodeQL pack")
1735
parser_codeql.add_argument(
1836
"--codeql-database",
1937
help="CodeQL database",
@@ -23,6 +41,9 @@
2341

2442
if __name__ == "__main__":
2543
args = parser.parse_args()
44+
owner, repo = args.github_repo.split("/", 1)
45+
# TODO: support GitHub Enterprise
46+
github = Octokit(owner=owner, repo=repo, token=args.github_token)
2647

2748
codeql_path = args.codeql_path or find_codeql()
2849

@@ -39,6 +60,25 @@
3960

4061
for database in databases:
4162
codeql = CodeQL(
42-
args.codeql_database,
63+
database,
4364
codeql_path,
4465
)
66+
print(codeql)
67+
68+
codeql_results = codeql.run("Dependencies.ql")
69+
results = parseDependencies(codeql_results)
70+
71+
print(f"Found {len(results)} dependencies.")
72+
73+
depgraph = exportDependencies(
74+
results,
75+
# git sha and ref
76+
sha=args.sha,
77+
ref=args.ref,
78+
# source is the codeql database
79+
source=database,
80+
)
81+
82+
github.submitDependencies(depgraph)
83+
84+
print("Done!")

codeqldepgraph/codeql.py

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,47 @@
11
import os
22
import glob
3-
import shutil
3+
import json
4+
import logging
45
import subprocess
56

7+
__HERE__ = os.path.dirname(os.path.abspath(__file__))
8+
__ROOT__ = os.path.abspath(os.path.join(__HERE__, ".."))
9+
610
CODEQL_LOCATIONS = [
711
"codeql",
812
# gh cli
913
"gh codeql",
14+
]
15+
# Actions
16+
CODEQL_LOCATIONS.extend(glob.glob("/opt/hostedtoolcache/CodeQL/*/x64/codeql/codeql"))
17+
# VSCode install
18+
CODEQL_LOCATIONS.extend(glob.glob(
19+
"/home/codespace/.vscode-remote/data/User/globalStorage/github.vscode-codeql/*/codeql/codeql"
20+
)),
21+
print(CODEQL_LOCATIONS)
22+
23+
CODEQL_DATABASE_LOCATIONS = [
24+
# local db
25+
".codeql/db",
1026
# Actions
11-
"/opt/hostedtoolcache/CodeQL/*/x64/codeql/codeql",
12-
# VSCode install
13-
"/home/codespace/.vscode-remote/data/User/globalStorage/github.vscode-codeql/*/codeql/codeql",
27+
"/home/runner/work/_temp/codeql_databases/",
1428
]
1529

16-
CODEQL_DATABASE_LOCATIONS = [".codeql/db", "/home/runner/work/_temp/codeql_databases/"]
30+
CODEQL_TEMP = os.path.join("/tmp", "codeqldepgraph")
31+
32+
logger = logging.getLogger("codeql")
1733

1834

1935
def find_codeql() -> str:
2036
"""Find the CodeQL executable"""
2137
for codeql in CODEQL_LOCATIONS:
22-
codeql = glob.glob(codeql)
23-
# test if the glob found anything
24-
if codeql:
25-
# test command works
26-
try:
27-
subprocess.run([codeql[0], "--version"], stdout=subprocess.PIPE)
28-
return codeql[0]
29-
except Exception as err:
30-
pass
38+
try:
39+
with open(os.devnull) as null:
40+
subprocess.run([codeql, "--version"], stdout=null, stderr=null)
41+
return codeql
42+
except Exception as err:
43+
pass
44+
print(f" >> {codeql}")
3145

3246
raise Exception("Could not find CodeQL executable")
3347

@@ -43,35 +57,94 @@ def find_codeql_databases() -> list:
4357

4458

4559
class CodeQL:
46-
def __init__(self, database: str, language: str, codeql_path: str = None):
60+
def __init__(self, database: str, codeql_path: str = None):
4761
self.database = database
48-
self.language = language
4962

5063
self.codeql_path = codeql_path or find_codeql()
51-
self.databases = []
64+
self.language = self.find_language()
65+
66+
self.pack_name = f"codeql-depgraph-{self.language}"
67+
68+
if not os.path.exists(CODEQL_TEMP):
69+
os.makedirs(CODEQL_TEMP)
5270

5371
def find_language(self) -> str:
5472
"""Find the language of the CodeQL database"""
55-
# find db folder
56-
db = glob.glob(os.path.join(self.database), "db-*")
73+
db = glob.glob(os.path.join(self.database, "db-*"))
74+
5775
if db:
58-
return db[0].split("-")[1]
76+
return db[0].split("-")[-1]
5977

6078
raise Exception("Could not find CodeQL database language")
6179

62-
def run_query(self, query):
80+
def run(self, query):
6381
"""Run a CodeQL query"""
64-
return subprocess.run(
65-
[
66-
self.codeql_path,
67-
"query",
68-
"run",
69-
query,
70-
"--database",
71-
self.database,
72-
"--language",
73-
self.language,
74-
],
75-
stdout=subprocess.PIPE,
76-
stderr=subprocess.PIPE,
82+
local_query = os.path.join(__ROOT__, "ql", self.language, query)
83+
if os.path.exists(local_query):
84+
full_query = local_query
85+
elif os.path.exists(query):
86+
full_query = query
87+
else:
88+
full_query = f"{self.pack_name}:{query}"
89+
90+
resultBqrs = os.path.join(
91+
self.database,
92+
"results",
93+
self.pack_name,
94+
query.replace(":", "/").replace(".ql", ".bqrs"),
7795
)
96+
97+
cmd = [
98+
self.codeql_path,
99+
"database",
100+
"run-queries",
101+
# use all the threads on system
102+
"--threads",
103+
"0",
104+
self.database,
105+
full_query,
106+
]
107+
logger.debug(f"Running: {' '.join(cmd)}")
108+
109+
output_std = os.path.join(CODEQL_TEMP, "runquery.txt")
110+
with open(output_std, "wb") as std:
111+
subprocess.run(cmd, stdout=std, stderr=std)
112+
113+
return self.readRows(resultBqrs)
114+
115+
def readRows(self, bqrsFile: str) -> list:
116+
generatedJson = os.path.join(CODEQL_TEMP, "out.json")
117+
output_std = os.path.join(CODEQL_TEMP, "rows.txt")
118+
119+
with open(output_std, "wb") as std:
120+
subprocess.run(
121+
[
122+
self.codeql_path,
123+
"bqrs",
124+
"decode",
125+
"--format",
126+
"json",
127+
"--output",
128+
generatedJson,
129+
bqrsFile,
130+
],
131+
stdout=std,
132+
stderr=std,
133+
)
134+
135+
with open(generatedJson) as f:
136+
results = json.load(f)
137+
138+
try:
139+
results["#select"]["tuples"]
140+
except KeyError:
141+
raise Exception("Unexpected JSON output - no tuples found")
142+
143+
rows = []
144+
for tup in results["#select"]["tuples"]:
145+
rows.extend(tup)
146+
147+
return rows
148+
149+
def __str__(self) -> str:
150+
return f"CodeQL(language='{self.language}', path='{self.database}')"

codeqldepgraph/dependencies.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from typing import *
2+
import os
3+
from datetime import datetime
4+
5+
from codeqldepgraph import __name__, __version__, __url__
6+
7+
8+
class Dependency:
9+
def __init__(self, **kwargs):
10+
self.namespace = kwargs.get("namespace")
11+
self.name = kwargs.get("name")
12+
self.version = kwargs.get("version")
13+
self.manager = kwargs.get("manager")
14+
self.path = kwargs.get("path")
15+
16+
@staticmethod
17+
def parse(data: str) -> "Dependency":
18+
filepath, name, version = data.split("<|>")
19+
20+
dep = Dependency(name=name, version=version, path=filepath).parseJava()
21+
return dep
22+
23+
def parseJava(self):
24+
if self.path and self.path.endswith(".jar"):
25+
# We assume that we are using Maven
26+
self.manager = "maven"
27+
28+
pathcomps = self.path.split(os.path.sep)
29+
jar = pathcomps[-1].replace(".jar", "")
30+
# split on last dash
31+
self.name, self.version = jar.rsplit("-", 1)
32+
33+
next = False
34+
for part in reversed(pathcomps):
35+
if next:
36+
self.namespace = part
37+
break
38+
elif part == self.name:
39+
next = True
40+
return self
41+
42+
def getName(self):
43+
if self.manager == "maven":
44+
return f"{self.namespace}.{self.name}"
45+
return self.name
46+
47+
def getPurl(self):
48+
return f"pkg:{self.manager}/{self.namespace}/{self.name}@{self.version}"
49+
50+
def __str__(self) -> str:
51+
"""Return a string representation of the dependency"""
52+
return self.getPurl()
53+
54+
55+
def parseDependencies(data: str) -> list[Dependency]:
56+
results = []
57+
58+
for line in data:
59+
results.append(Dependency.parse(line))
60+
return results
61+
62+
63+
def exportDependencies(dependencies: list[Dependency], **kwargs):
64+
resolved = {}
65+
for dep in dependencies:
66+
name = dep.getName()
67+
purl = dep.getPurl()
68+
resolved[name] = {"package_url": purl}
69+
data = {
70+
"version": 0,
71+
"sha": kwargs.get("sha"),
72+
"ref": kwargs.get("ref"),
73+
"job": {"correlator": __name__, "id": __name__},
74+
"detector": {"name": __name__, "version": __version__, "url": __url__},
75+
"scanned": datetime.now().isoformat(),
76+
"manifests": {
77+
__name__: {
78+
"name": __name__,
79+
"file": {
80+
"source_location": "codeql",
81+
},
82+
"resolved": resolved,
83+
}
84+
},
85+
}
86+
return data

codeqldepgraph/octokit.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import *
2+
3+
import requests
4+
5+
6+
class Octokit:
7+
def __init__(self, owner: str, repo: str, token: str, url="https://api.github.com"):
8+
self.owner = owner
9+
self.repo = repo
10+
self.token = token
11+
self.url = url
12+
13+
def submitDependencies(self, dependencies: dict):
14+
"""Submit dependencies to GitHub"""
15+
url = f"{self.url}/repos/{self.owner}/{self.repo}/dependency-graph/snapshots"
16+
resp = requests.post(
17+
url,
18+
json=dependencies,
19+
headers={
20+
"Accept": "application/vnd.github+json",
21+
"Authorization": f"token {self.token}",
22+
},
23+
)
24+
if resp.status_code != 201:
25+
raise Exception(
26+
f"Failed to submit dependencies: {resp.status_code} {resp.text}"
27+
)

0 commit comments

Comments
 (0)