Skip to content
This repository has been archived by the owner on Jul 3, 2024. It is now read-only.

Commit

Permalink
added dpia results functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
simonarnell committed May 15, 2020
1 parent b94905f commit a095190
Show file tree
Hide file tree
Showing 7 changed files with 382 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ A free web-based Data Protection Impact Assessment Tool to assist organisations

An instance of the tool is hosted on [GitHub Pages](https://simonarnell.github.io/GDPRDPIAT/) for preview. Please note, to demonstrate how one might use this project for self-service data protection impact assessments within a DevOps team, this project uses the [Staticman](https://staticman.net) project, a useful tool for static sites such as GitHub pages that allows user generated content, in our case GDPR DPIAs, to be committed into a GitHub repository, for this project the submissions are committed on a branch called [staticman](https://github.com/simonarnell/GDPRDPIAT/tree/staticman). The data protection impact assessments could then be used within a GitOps workflow to allow a security expert within the wider DevSecOps team to provide more in-depth analysis and a set of recommendations for a project or sprint.

The submitted DPIAs can be reviewed on the [results](https://simonarnell.github.io/GDPRDPIAT/results.html) page. This queries the GitHub API for the contents of the staticman branch of this repository, the DPIAs are then collated and charts dynamically generated for analysis.

## Disclaimer

Please use this only for what it is intended, a first pass assessment, seek separate legal and privacy advice for a more formal assessment of your organisation’s position. **I accept no liability.**
21 changes: 21 additions & 0 deletions resources/css/results.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
article {
padding-left: 10px;
padding-right: 10px;
}

.tick {
color: black;
}

.response {
fill: #003399;
}

.legend.swatch {
stroke: white;
stroke-width: 2;
}

.legend.label {
text-anchor: middle;
}
169 changes: 169 additions & 0 deletions resources/js/charts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
export class Chart {
constructor(config, data) {
this.config = config
this.data = data
return this
}

generateXScale(accessor) {
this.xScale = d3.scaleBand()
.domain(this.data.responseSet.choices.map(accessor))
.range([this.config.margin.left, this.config.width - this.config.margin.right])
.paddingOuter(this.config.padding.outer)
.paddingInner(this.config.padding.inner)
}

generateXLabels() {
d3.select(`#${this.data.name}`)
.append('svg')
.attr('viewBox', [0, 0, this.config.width, this.config.height])
.append('g').attr('class', 'x ticks')
.call(d3.axisBottom(this.xScale))
.selectAll('.tick text')
.call(this.wrap, this.xScale.bandwidth())
this.config.margin.bottom += d3.max(d3.select(`#${this.data.name}`).selectAll('.tick text').nodes(), (text) => text.getBBox().height)
}

wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1,
y = text.attr('y'),
dy = parseFloat(text.attr('dy')),
tspan = text.text(null).append('tspan').attr('x', 0).attr('y', y).attr('dy', dy + 'em');
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(' '));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(' '));
line = [word];
tspan = text.append('tspan').attr('x', 0).attr('y', y).attr('dy', ++lineNumber * lineHeight + dy +'em').text(word);
}
}
});
}
}

export class BarChart extends Chart {
constructor(config, data) {
super(config, data)
this.generateXScale(d => Object.keys(d)[0])
this.generateXLabels()
this.yScale = d3.scaleLinear()
.domain([0, this.data.responseSet.choices.reduce((acc, choice) => Object.values(choice)[0] > acc ? Object.values(choice)[0] : acc, 0)]).nice()
.range([this.config.height - this.config.margin.bottom, this.config.margin.top])
d3.select(`#${this.data.name}`)
.select('svg')
.call(svg => svg
.append('g')
.attr('class', 'axes')
.call(g => g
.append('g')
.attr('class', 'axis x')
.attr('transform', `translate(0, ${this.config.height - this.config.margin.bottom})`)
.append(() => d3.select(`#${this.data.name}`).select('.x.ticks').remove().node()))
.call(g => g
.append('g')
.attr('class', 'axis y')
.attr('transform', `translate(${this.config.margin.left}, 0)`)
.append('g').attr('class', 'y ticks').call(d3.axisLeft(this.yScale))))
.call(svg => svg
.append('g')
.attr('class', 'responses')
.selectAll('rect')
.data(this.data.responseSet.choices)
.join('rect')
.attr('class', 'response')
.attr('width', this.xScale.bandwidth())
.attr('height', d => this.yScale.range()[0] - this.yScale(Object.values(d)[0]))
.attr('x', d => this.xScale(Object.keys(d)[0]))
.attr('y', d => this.yScale(Object.values(d)[0])))
return this
}
}

export class StackedBarChart extends Chart {
constructor(config, data) {
super(config, data)

this.generateXScale(d => Object.values(d)[0])
this.generateXLabels()

let keys = [...new Set(this.data.responseSet.choices.map(({ question, ...responses}) => { return { ...responses }})
.reduce((acc, responses) => acc = acc.concat(Object.keys(responses)),[]))]

this.colour = d3.scaleOrdinal()
.domain(keys.map((d, i)=> i))
.range(['#003399','#ffcc00'])

d3.select(`#${this.data.name}`).select('svg')
.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${this.config.width - this.config.margin.right - this.config.legend.width}, ${this.config.margin.top})`)
.selectAll('g')
.data(this.colour.domain())
.join('g')
.attr('transform', (d, i, arr) => `translate(${i * this.config.legend.width / arr.length}, 0)`)
.call(g => g
.append('rect')
.attr('class', 'legend swatch')
.attr('height', this.config.legend.height)
.attr('width', (d, i, arr) => this.config.legend.width / arr.length)
.style('fill', d => this.colour(d)))
.call(g => g
.append('text')
.attr('transform', `translate(0, ${this.config.legend.height})`)
.attr('class', 'legend label')
.attr('x', (d, i, arr) => this.config.legend.width / arr.length / 2)
.attr('y', '1em')
.text(d => keys[d]))

this.config.margin.top += d3.select('.legend').node().getBBox().height

let stack = d3.stack()
.keys(keys)
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone)

let series = stack(this.data.responseSet.choices);

this.yScale = d3.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d, d => d[1]))]).nice()
.range([this.config.height - this.config.margin.bottom, this.config.margin.top])

d3.select(`#${this.data.name}`)
.select('svg')
.call(svg => svg
.append('g')
.attr('class', 'axes')
.call(g => g
.append('g')
.attr('class', 'axis x')
.attr('transform', `translate(0, ${this.config.height - this.config.margin.bottom})`)
.append(() => d3.select(`#${this.data.name}`).select('.x.ticks').remove().node()))
.call(g => g
.append('g')
.attr('class', 'axis y')
.attr('transform', `translate(${this.config.margin.left}, 0)`)
.append('g').attr('class', 'y ticks').call(d3.axisLeft(this.yScale))))
.call(svg => svg
.append('g')
.selectAll('g')
.data(series)
.join('g')
.style('fill', (d, i) => this.colour(i))
.selectAll('rect')
.data(d => d)
.join('rect')
.attr('x', (d, i) => this.xScale(this.data.responseSet.choices[i].question))
.attr('y', d => this.yScale(d[1]))
.attr('height', d => this.yScale(d[0]) - this.yScale(d[1]))
.attr('width', this.xScale.bandwidth()))
return this
}
}
113 changes: 113 additions & 0 deletions resources/js/results.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { BarChart, StackedBarChart } from './charts.js';
import { Table } from './table.js';

var questionsFetch = fetch('resources/data/questions.json')
.then(res => res.json())

var responsesFetch = fetch('https://api.github.com/repos/simonarnell/GDPRDPIAT/contents/_data/dpia?ref=staticman')
.then(res => res.json())
.then(json => Promise.all(json.map(dpia => {
return fetch(dpia.download_url)
.then(res => res.json())
})))

var questions = [], responses = [];
Promise.all([questionsFetch, responsesFetch]).then(vals => {
vals[0].pages.map(page => page.questions.map(question => {
questions.push(question)
responses[question.name] = {
...(()=>{
let choices = {}
if(question.choices !== undefined) {
choices = question.choices.reduce((acc, choice) => {
acc[choice.value] = 0
return acc
}, {})
} else if(question.type == 'matrix') {
choices = question.rows.reduce((acc, row) => {
acc[row.value] = question.columns.reduce((acc, column) => {
acc[column.value] = 0
return acc
}, {})
return acc
}, {})
}
return choices
})(),
...vals[1].map(response => {
let formattedResponse = [];
if(response[question.name] !== undefined) {
if(question.type == 'checkbox') formattedResponse = response[question.name].split(',')
else formattedResponse = [response[question.name]]
} else if(question.type == 'matrix') formattedResponse = question.rows.map(row => response[question.name + String.fromCharCode(97 + (row.value - 1))])
return formattedResponse
}).reduce((acc, responses, idx) => {
if(idx == 0 && question.type == 'matrix')
acc = question.rows.reduce((acc, row) => {
acc[row.value] = question.columns.reduce((acc, column) => {
acc[column.value] = 0
return acc
}, {})
return acc
}, {})
let countedResponse = responses.reduce((acc, response, idx) => {
if(question.type == 'matrix') {
if(response == undefined) return acc
if(!acc[idx+1]) acc[idx+1] = {};
acc[idx+1][response] = !!acc[idx+1][response] ? acc[idx+1][response] + 1 : 1
} else acc[response] = !!acc[response] ? acc[response] + 1 : 1
return acc
}, {})
Object.keys(countedResponse).forEach(item => {
if(question.type == 'matrix') {
if(!acc[item]) acc[item] = {}
Object.keys(countedResponse[item]).forEach(response => acc[item][response] = !!acc[item] ? acc[item][response] + countedResponse[item][response] : 1)
} else acc[item] = !!acc[item] ? acc[item] + countedResponse[item] : 1
})
return acc
}, {})
}
}))
questions.map(({choices, columns, name, rows, title, type}) => {
let config = {
height: 540,
width: 1920,
margin: {
top: 10,
left: 20,
right: 10,
bottom: 10
},
padding: {
inner: 0.03,
outer: 0.03
},
legend: {
width: 240,
height: 50
}
}
let responseSet = {}
if(type == 'matrix') {
responseSet.choices = rows.map(row => {
return {
question: row.text,
...columns.reduce((acc, column) => {
acc[column.text] = responses[name][row.value][column.value]
return acc
}, {})
}
})
} else if (choices !== undefined) {
responseSet.choices = choices.map(choice => {
return { [choice.text]: responses[name][choice.value] }
})
} else return
let section = document.createElement('section')
section.id = name
document.getElementById('results').appendChild(section)
new Table({ name, title })
if(type == 'matrix') new StackedBarChart(config, { name, type, responseSet })
else new BarChart(config, { name, type, responseSet })
})
})
22 changes: 22 additions & 0 deletions resources/js/table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export class Table {
constructor(data) {
this.data = data;
let table = document.createElement('table')
let tr = document.createElement('tr')
let td = document.createElement('td')
let h3 = document.createElement('h3')
let match = this.data.name.match(/(question)(([0-9])+[a-zA-Z]*)/)
h3.textContent = `${match[1].slice(0,1).toUpperCase()}${match[1].slice(1, match[1].length)} ${match[2]}`
td.appendChild(h3)
tr.appendChild(td)
table.appendChild(tr)
tr = document.createElement('tr')
td = document.createElement('td')
let h4 = document.createElement('h4')
h4.textContent = this.data.title
td.appendChild(h4)
tr.appendChild(td)
table.appendChild(tr)
document.getElementById(this.data.name).appendChild(table)
}
}
52 changes: 52 additions & 0 deletions results.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!doctype html>
<html>
<head>
<title>General Data Protection Regulation - Data Protection Impact Assessment Tool - Results</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="resources/css/gdprdpiat.css">
<link rel="stylesheet" href="resources/css/results.css">
<script src="https://d3js.org/d3.v5.min.js"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-127874216-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-127874216-1');
</script>
<script type="application/ld+json">
{
"@context":"http://schema.org",
"@type":"WebSite",
"name":"General Data Protection Regulation (GDPR) Data Protection Impact Assessment (DPIA) Tool",
"author": {
"@type": "Person",
"name": "Simon Arnell",
"url": "https://simonarnell.github.io/"
},
"description":"A GDPR Data Protection Impact Assessment (DPIA) tool to assist organisations to evaluate data protection risks with respect to the EU's General Data Protection Regulation. 🇪🇺",
"mentions" : {
"@type": "Legislation",
"name": "General Data Protection Regulation",
"legislationType":"regulation",
"legislationIdentifier": "2016/679",
"sameAs": "https://en.wikipedia.org/wiki/General_Data_Protection_Regulation"
},
"url": "https://simonarnell.github.io/GDPRDPIAT/",
"sameAs": "https://github.com/simonarnell/GDPRDPIAT"
}
</script>
<script type="module" src="resources/js/results.js"></script>
</head>
<body>
<header class="header">
<a class="forkme" href="https://github.com/simonarnell/GDPRDPIAT"><img width="149" height="149" src="https://github.blog/wp-content/uploads/2008/12/forkme_left_darkblue_121621.png?resize=149%2C149" class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></a>
<h1>EU General Data Protection Regulation (GDPR) Data Protection Impact Assessment (DPIA) Tool</h1>
</header>
<main id="dpiat">
<article id="results">
<h2>Results</h2>
</article>
</main>
<footer class="footer">Copyright (c) 2020 <a href="https://simonarnell.github.io/">Simon Arnell</a> - Licensed under the <a href="https://github.com/simonarnell/GDPRDPIAT/blob/master/LICENSE.md">MIT License</a></footer>
</body>
</html>
3 changes: 3 additions & 0 deletions sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
<url>
<loc>https://simonarnell.github.io/GDPRDPIAT/</loc>
</url>
<url>
<loc>https://simonarnell.github.io/GDPRDPIAT/results.html</loc>
</url>
</urlset>

0 comments on commit a095190

Please sign in to comment.