Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report graph #41

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 66 additions & 127 deletions web/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import typing as t
from fastapi import Depends, FastAPI, Header, HTTPException, Response
from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from starlette.responses import FileResponse
from sqlalchemy.orm import Session

from . import models, schemas, crud, user_controller
Expand All @@ -26,6 +28,9 @@
allow_headers=["*"],
)

app.mount("/static", StaticFiles(directory="web/static",html=True), name="static")
app.mount("/styles", StaticFiles(directory="web/styles"), name="styles")

get_bearer_token = HTTPBearer(auto_error=False)

async def get_token(
Expand Down Expand Up @@ -126,6 +131,8 @@ def attestations_by_out(output_path: str, db: Session = Depends(get_db)):

def report_out_paths(report):
paths = []
root = report['metadata']['component']['bom-ref']
paths.append(root)
for component in report['components']:
for prop in component['properties']:
if prop['name'] == "nix:out_path":
Expand Down Expand Up @@ -159,125 +166,6 @@ def printtree(root, deps, results, cur_indent=0, seen=None):
#result = result + "\n " + d
return result

def htmltree(root, deps, results):
def icon(result):
if result == "No builds":
return "❔ "
elif result == "One build":
return "❎ "
elif result == "Partially reproduced":
return "❕ "
elif result == "Successfully reproduced":
return "✅ "
elif result == "Consistently nondeterministic":
return "❌ "
else:
return ""
def generatetree(root, seen):
if root in seen:
return f'<summary title="{root}">...</summary>'
seen[root] = True;

result = f'<summary title="{root}">'
if root in results:
result = result + f'<span title="{results[root]}">' + icon(results[root]) + "</span>" + root[44:] + " "
else:
result = result + root[44:]
result = result + "</summary>\n"
result = result + "<ul>"
for dep in deps:
if dep['ref'] == root and 'dependsOn' in dep:
for d in dep['dependsOn']:
result += f'<li><details class="{d}" open>'
result += generatetree(d, seen)
result += "</details></li>"
result = result + "</ul>"
return result
tree = generatetree(root, {})
return '''
<html>
<head>
<style>
.tree{
--spacing : 1.5rem;
--radius : 8px;
}

.tree li{
display : block;
position : relative;
padding-left : calc(2 * var(--spacing) - var(--radius) - 2px);
}

.tree ul{
margin-left : calc(var(--radius) - var(--spacing));
padding-left : 0;
}

.tree ul li{
border-left : 2px solid #ddd;
}

.tree ul li:last-child{
border-color : transparent;
}

.tree ul li::before{
content : '';
display : block;
position : absolute;
top : calc(var(--spacing) / -2);
left : -2px;
width : calc(var(--spacing) + 2px);
height : calc(var(--spacing) + 1px);
border : solid #ddd;
border-width : 0 0 2px 2px;
}

.tree summary{
display : block;
cursor : pointer;
}

.tree summary::marker,
.tree summary::-webkit-details-marker{
display : none;
}

.tree summary:focus{
outline : none;
}

.tree summary:focus-visible{
outline : 1px dotted #000;
}

.tree li::after,
.tree summary::before{
content : '';
display : block;
position : absolute;
top : calc(var(--spacing) / 2 - var(--radius));
left : calc(var(--spacing) - var(--radius) - 1px);
width : calc(2 * var(--radius));
height : calc(2 * var(--radius));
border-radius : 50%;
background : #ddd;
}

</style>
</head>
''' + f'''
<body>
<ul class="tree">
<li>
{tree}
</li>
</ul>
</body>
</html>
'''

@app.get("/reports/{name}")
def report(
name: str,
Expand All @@ -293,20 +181,71 @@ def report(
content=json.dumps(report),
media_type='application/vnd.cyclonedx+json')

paths = report_out_paths(report)

root = report['metadata']['component']['bom-ref']
results = crud.path_summaries(db, paths)

if 'text/html' in accept:
return Response(
content=htmltree(root, report['dependencies'], results),
media_type='text/html')
return FileResponse('web/static/report.html')
else:
paths = report_out_paths(report)
root = report['metadata']['component']['bom-ref']
results = crud.path_summaries(db, paths)
return Response(
content=printtree(root, report['dependencies'], results),
media_type='text/plain')

@app.get("/reports/{name}/graph-data.json")
def graph_data(
name: str,
db: Session = Depends(get_db),
):
report = crud.report(db, name)
if report == None:
raise HTTPException(status_code=404, detail="Report not found")

legend = [
"No builds",
"One build",
"Partially reproduced",
"Successfully reproduced",
"Consistently nondeterministic",
];
color = [
"#eeeeee",
"#aaaaaa",
"#eeaaaa",
"#00ee00",
"#ee0000",
];
categories = []
for category in legend:
categories.append({
"name": category,
"base": category,
"keyword": {},
})
paths = report_out_paths(report)
results = crud.path_summaries(db, paths)

nodes = []
for path in paths:
nodes.append({
"name": path,
"category": results[path],
})
links = []
for dep in report['dependencies']:
for dependee in dep['dependsOn']:
links.append({
"source": paths.index(dep['ref']),
"target": paths.index(dependee),
})
return {
"type": "force",
"legend": legend,
"categories": categories,
"color": color,
"nodes": nodes,
"links": links,
}

@app.put("/reports/{name}")
def define_report(
name: str,
Expand Down
45 changes: 45 additions & 0 deletions web/static/echarts.min.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions web/static/jquery.min.js

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions web/static/report.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="/static/echarts.min.js"></script>
<!-- TODO perhaps replace -->
<script src="/static/jquery.min.js "></script>
<link rel="stylesheet" href="/styles/index.css">
<style>
body {
padding: 3em;
}
</style>
</head>
<body>
<h1>Reproducibility report</h1>
<p>
This is an report based on the experimental
<a href="https://github.com/JulienMalka/lila">lila</a>
hash collection infrastructure for NixOS. The graph below shows whether hashes
collected from independent rebuilders agree. Paths are divided into five categories:
<ul>
<li><b>No builds:</b> no builders provided a hash for this path. Populating the report
might still be in progress, or <a href="https://github.com/JulienMalka/lila/issues/39">#39</a>
<li><b>One build:</b> this path appeared only once. Populating the report
might still be in progress, or <a href="https://github.com/JulienMalka/lila/issues/40">#40</a>
<li><b>Partially reproduced:</b> some but, not all, builders agreed on the hash. At this point this likely just means the build is nondeterministic, but it might be good to double-check for malicious builds.
<li><b>Successfully reproduced:</b> several builders reported the same hash.
<li><b>Consistently nondeterministic:</b> no builders agreed on the hash. The build is likely nondeterministic.
</ul>
</p>

<div id="main" style="width: 100%; height: 800"></div>
<script>
var option;
const myChart = echarts.init(document.getElementById('main'))
myChart.showLoading();
myChart.showLoading();
$.get(document.location.pathname + '/graph-data.json', function (webkitDep) {
console.log('loaded', webkitDep);
myChart.hideLoading();
option = {
color: webkitDep.color,
legend: {
data: webkitDep.legend
},
series: [
{
type: 'graph',
layout: 'force',
edgeSymbol: ['circle', 'arrow'],
animation: true,
label: {
position: 'right',
formatter: '{b}'
},
draggable: true,
data: webkitDep.nodes.map(function (node, idx) {
node.id = idx;
node.value = 1;
return node;
}),
categories: webkitDep.categories,
// a bit overwhelming - can we add a delay?
//emphasis: {
// focus: 'adjacency'
//},
force: {
edgeLength: 5,
repulsion: 20,
gravity: 0.2
},
edges: webkitDep.links
}
]
};
myChart.on('click', function(params) {
if (params.dataType == 'node') {
alert(params.name);
}
});
myChart.setOption(option);
});
</script>
</body>
</html>
Binary file added web/styles/fonts/FiraMono-Regular.ttf
Binary file not shown.
Binary file added web/styles/fonts/Overpass-ExtraBold.ttf
Binary file not shown.
Binary file added web/styles/fonts/Roboto-Bold.ttf
Binary file not shown.
Binary file added web/styles/fonts/Roboto-Light.ttf
Binary file not shown.
Binary file added web/styles/fonts/Roboto-LightItalic.ttf
Binary file not shown.
Loading