Build a GRC Risk Scoring Engine

Build a Python risk engine that auto-deploys a live compliance dashboard.

Introduction

30 Second Summary

Every organization faces security risks, but most compliance teams still track them in static spreadsheets that go stale the moment they are saved. When a CISO asks "where are we most exposed?", the answer shouldn't take a week of manual analysis.

In this project, you will build a Python-based risk scoring engine that transforms a CSV control register into an interactive compliance dashboard, then deploy it to a live URL with a CI/CD pipeline that auto-updates every time you push a change.

What You'll Build

Push a change to your control data on GitHub, and within minutes a live dashboard recalculates every risk score and redeploys itself. A hiring manager clicks your link and immediately explores interactive heatmaps, coverage charts, and gap analysis without installing anything.

By the end of this project, you'll have:

  • A working risk scoring engine you can run from your terminal to compute residual risk for every control, mapped to SOC 2 Trust Services Criteria and NIST CSF 2.0.
  • A live interactive dashboard at a public URL where anyone can hover over a risk heatmap, compare framework coverage, and see exactly which controls need remediation.
  • A GitHub Actions pipeline that proves continuous compliance capability. Edit a control's effectiveness score, push to GitHub, and the dashboard regenerates and redeploys automatically.
  • Secret Mission: Add a vendor risk assessment dimension with third-party risk scoring and a dedicated vendor risk section on the dashboard.

Are there any prerequisites?

You need Python 3.9 or higher installed, VS Code (or any code editor), and a GitHub account. No prior GRC or compliance experience is required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up the Project

Before you can write any risk-scoring logic, you need a properly structured project with the right Python packages installed and a Git repository ready for version control.

In this step, you'll create the project skeleton, install your data and visualization dependencies, and push your initial commit to a public GitHub repository.

In this step, get ready to:
  • Create the grc-risk-engine/ project directory with data/ and src/ subdirectories.
  • Install pandas and plotly Python packages.
  • Push your initial commit to a public GitHub repository.
Create the project directory and initialize Git
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop and create the project folder by running these commands:
cd ~/Desktop
mkdir grc-risk-engine
cd grc-risk-engine
  • Create the subdirectories and initialize a Git repository by running:
mkdir data src
git init

You should see a message that says Initialized empty Git repository followed by a path. This confirms your project is now version-controlled.

  • Confirm your folder structure by running:
ls

You should see data and src listed in the output.

Don't see both folders?

Make sure you ran mkdir data src with a space between the two names. Without the space, you would create a single folder named datasrc.

If you accidentally created folders in the wrong location, run pwd to check your current directory. You should see a path ending in grc-risk-engine.

Help me figure out what went wrong

  • Open VS Code with your project folder by running:
code .

Getting command not found?

On macOS, the code command needs to be installed into your PATH manually. Open VS Code through your search bar instead, then press Cmd+Shift+P, type Shell Command: Install 'code' command in PATH, and select it. Restart your terminal, then try code . again.

Alternatively, open VS Code via your search bar, click File in the top menu, click Open Folder, and select the grc-risk-engine folder on your Desktop.

Help me open my project in VS Code

VS Code opens with your grc-risk-engine folder loaded. You should see data and src in the Explorer sidebar on the left.

  • In the VS Code Explorer sidebar, click the New File icon (next to your project name).
  • Type requirements.txt as the file name and press Enter.
  • Paste the following content into the file:
pandas==2.3.3
plotly==6.9.0

What does this file do?

A requirements.txt file lists the exact package versions your project needs. Pinning versions (like pandas==2.3.3) ensures anyone who clones your repo gets the same working environment.

You are using pandas for data manipulation and Plotly for interactive visualizations that export as self-contained HTML.

  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).

You should see requirements.txt appear in your Explorer sidebar alongside the data and src folders.

✔️ Awesome, I've got everything!

Great. Double check you have saved requirements.txt and you can see all three items (data, src, requirements.txt) in the Explorer sidebar.

ⓧ I'd like to double check the full code

Here is the complete requirements.txt file:

pandas==2.3.3
plotly==6.9.0
Install Python dependencies

With your requirements file in place, you can install both packages in one command.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, below Escape).
  • Install the project dependencies by running:
pip install -r requirements.txt

You should see download and installation output for both pandas and plotly, ending with a success message.

Seeing a permission error or pip not found?

If you see a permission error, try running pip install --user -r requirements.txt instead.

If pip is not found, try pip3 install -r requirements.txt. Some systems use pip3 for Python 3 packages.

Help me troubleshoot my pip install error

Before you verify, what do you think will happen if the packages installed correctly and you try to import them in Python?

  • Verify the packages are importable by running:
python -c "import pandas; import plotly; print('Ready')"

You should see Ready printed to the terminal. This confirms both packages are installed and accessible.

Seeing ModuleNotFoundError?

If you see ModuleNotFoundError, the pip that installed the packages might point to a different Python version than the python command. Try running python3 -c "import pandas; import plotly; print('Ready')" instead.

If that also fails, try reinstalling with python3 -m pip install -r requirements.txt to ensure the packages go to the correct Python.

Help me fix this import error

Create the GitHub repository and connect it

Your code needs a remote home so the GitHub Actions CI/CD pipeline can deploy your dashboard later. A public repository also lets hiring managers view your work directly.

  • Open github.com in your browser and sign in to your account.
  • Click the + icon in the top-right corner of the page.
  • Click New repository.
  • Enter grc-risk-engine as the Repository name.
  • Select Public for visibility.
  • Leave all other options unchecked (no README, no .gitignore, no license).
  • Click Create repository.

Why no README or .gitignore from GitHub?

You already initialized Git locally with git init. If GitHub creates a README, it adds a commit that conflicts with your local history. Starting with an empty remote lets you push your local work cleanly.

GitHub shows a setup page with commands for your new empty repository. You will use those now.

  • Switch back to the VS Code integrated terminal.
  • Stage and commit your files, then connect and push to GitHub by running these commands:
git add .
git commit -m "Initial project setup"
git branch -M main
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/grc-risk-engine.git
git push -u origin main

What do these commands do?

  • git add . stages all files in the current directory for commit.
  • git commit -m saves a snapshot of your staged files with a message.
  • git branch -M main renames your default branch to main.
  • git remote add origin connects your local repo to the GitHub remote.
  • git push -u origin main uploads your code to GitHub and sets main as the default upstream branch.

You should see output ending with a line like Branch 'main' set up to track remote branch 'main' from 'origin'.

  • Refresh your GitHub repository page in the browser.

You should see your requirements.txt file along with the data and src directories listed on the repository page.

Push failed or authentication error?

If you see a permission or authentication error, GitHub requires a personal access token or SSH key for pushing. Visit your GitHub Settings page, then Developer settings, then Personal access tokens to generate a token with repo scope. Use this token as your password when prompted.

If you see an error about the remote already existing, run git remote remove origin first, then re-run the git remote add origin command with the correct URL.

Help me resolve my push error

Your project foundation is in place. You have a version-controlled directory, working Python packages, and a live GitHub repository ready for code.

Next up, you'll create a realistic control register and write the risk scoring formula that turns raw compliance data into quantified residual risk scores.

Build the Risk Scoring Engine

Your project structure is ready and your dependencies are installed. Now it is time to build the core of your GRC platform: a risk scoring engine that takes raw compliance data and computes quantitative residual risk.

Every enterprise risk register needs two things: realistic control data mapped to compliance frameworks, and a formula that turns subjective ratings into actionable numbers. In this step, you will create both and see your first scored output in the terminal.

In this step, get ready to:
  • Create a control register CSV with 12 controls mapped to SOC 2 and NIST CSF 2.0.
  • Write a Python scoring engine that computes residual risk, detects gaps, and analyzes framework coverage.
  • Run the engine and observe scored results printed to your terminal.
Create the control register

A control register is the structured dataset that feeds your risk engine. Each row represents a security control mapped to both SOC 2 Trust Services Criteria categories and NIST CSF 2.0 functions, with numeric scores for impact, likelihood, and current effectiveness.

  • Create a new file called controls.csv inside your data/ directory and paste the following content:
control_id,control_name,soc2_category,nist_csf_function,impact,likelihood,effectiveness
CTRL-001,MFA Enforcement,Security,Protect,5,3,0.85
CTRL-002,Data Encryption at Rest,Confidentiality,Protect,5,4,0.90
CTRL-003,Incident Response Plan,Security,Respond,4,3,0.70
CTRL-004,Backup Recovery Testing,Availability,Recover,5,4,0.75
CTRL-005,Access Review Quarterly,Security,Govern,4,3,0.40
CTRL-006,Vulnerability Scanning,Security,Detect,4,4,0.80
CTRL-007,Change Management Process,Processing Integrity,Govern,3,3,0.65
CTRL-008,Data Retention Policy,Privacy,Identify,3,2,0.30
CTRL-009,Network Segmentation,Security,Protect,5,5,0.60
CTRL-010,Security Awareness Training,Security,Govern,3,4,0.45
CTRL-011,Log Monitoring and Alerting,Security,Detect,4,4,0.35
CTRL-012,Disaster Recovery Plan,Availability,Recover,5,3,0.80

What does this data represent?

Each row is a security control your fictional organization has implemented. The soc2_category column maps to one of the five SOC 2 Trust Services Criteria: Security, Availability, Processing Integrity, Confidentiality, and Privacy.

The nist_csf_function column maps to the six NIST CSF 2.0 functions: Govern, Identify, Protect, Detect, Respond, and Recover. The effectiveness column (0.0 to 1.0) represents how well each control is currently performing.

  • Save data/controls.csv.
  • Confirm the file exists by running:
ls data/

You should see controls.csv listed in the output.

Don't see controls.csv?

  • Make sure you are running the command from inside the grc-risk-engine folder. Run pwd to check your current directory.
  • Confirm you saved the file in data/ and not in the project root.

Still stuck? Ask for help.

Write the risk scoring functions

Now you will build the engine that reads the control register and applies the quantitative risk formula: Residual Risk = Impact x Likelihood x (1 - Effectiveness).

  • Create a new file called risk_engine.py inside your src/ directory and paste this first section:
import pandas as pd
from pathlib import Path


DATA_DIR = Path(__file__).resolve().parent.parent / "data"
GAP_THRESHOLD = 0.5


def load_controls(filepath=None):
    if filepath is None:
        filepath = DATA_DIR / "controls.csv"
    return pd.read_csv(filepath)

What does this code do?

  • DATA_DIR calculates the path to your data/ folder relative to the script location, so the engine works regardless of where you run it from.
  • GAP_THRESHOLD sets the effectiveness cutoff below which a control is considered a gap. You will use this later.
  • load_controls() reads your CSV into a pandas DataFrame for analysis.
  • Add the scoring and classification functions below load_controls() in the same file:
def compute_residual_risk(df):
    df = df.copy()
    df["residual_risk"] = df["impact"] * df["likelihood"] * (1 - df["effectiveness"])
    df["residual_risk"] = df["residual_risk"].round(2)
    df["risk_level"] = df["residual_risk"].apply(classify_risk)
    return df


def classify_risk(score):
    if score > 12:
        return "Critical"
    elif score > 8:
        return "High"
    elif score > 4:
        return "Medium"
    else:
        return "Low"

What does this code do?

  • compute_residual_risk() applies the formula to every row. A control with high impact and likelihood but low effectiveness produces a high residual risk score.
  • classify_risk() translates numeric scores into human-readable tiers: Critical (above 12), High (above 8), Medium (above 4), and Low (4 or below).
  • Add the main execution block at the bottom of src/risk_engine.py:
if __name__ == "__main__":
    controls = load_controls()
    scored = compute_residual_risk(controls)

    print("=" * 60)
    print("GRC RISK SCORING ENGINE - RESULTS")
    print("=" * 60)
    print(f"\n{'ID':<10}{'Control':<30}{'Residual Risk':<15}{'Level'}")
    print("-" * 60)
    for _, row in scored.iterrows():
        print(
            f"{row['control_id']:<10}{row['control_name']:<30}"
            f"{row['residual_risk']:<15}{row['risk_level']}"
        )

What does this code do?

The if __name__ == "__main__" block runs only when you execute the script directly. It loads the CSV, scores every control, and prints a formatted table to your terminal.

  • Save src/risk_engine.py.
  • Run the engine by executing this command from your grc-risk-engine folder:
python src/risk_engine.py

You should see a formatted table showing each control with its computed residual risk and risk level. For example, CTRL-001 (MFA Enforcement) should show a residual risk of 2.25 and a level of Low.

Seeing a ModuleNotFoundError?

  • Make sure your virtual environment is activated. You should see (venv) or similar prefix in your terminal prompt.
  • If you see No module named 'pandas', re-run pip install -r requirements.txt from inside your project folder.

Need more help? Ask here.

Add gap detection and framework coverage analysis

Raw risk scores tell you how dangerous each control gap is, but they do not tell you where your compliance program is weakest overall. You need two more functions: one to flag controls below the effectiveness threshold, and one to calculate average coverage by framework.

  • In src/risk_engine.py, add the detect_gaps() function below classify_risk():
def detect_gaps(df, threshold=GAP_THRESHOLD):
    gaps = df[df["effectiveness"] < threshold].copy()
    gaps = gaps.sort_values("residual_risk", ascending=False)
    return gaps

What does this code do?

detect_gaps() filters for controls whose effectiveness is below 0.5 (the GAP_THRESHOLD constant). It returns them sorted by residual risk so the most dangerous gaps appear first.

  • Add the compute_coverage() function below detect_gaps():
def compute_coverage(df):
    soc2_coverage = (
        df.groupby("soc2_category")
        .agg(
            avg_effectiveness=("effectiveness", "mean"),
            avg_residual_risk=("residual_risk", "mean"),
            control_count=("control_id", "count"),
        )
        .round(2)
        .reset_index()
    )
    nist_coverage = (
        df.groupby("nist_csf_function")
        .agg(
            avg_effectiveness=("effectiveness", "mean"),
            avg_residual_risk=("residual_risk", "mean"),
            control_count=("control_id", "count"),
        )
        .round(2)
        .reset_index()
    )
    return soc2_coverage, nist_coverage

What does this code do?

compute_coverage() groups controls by SOC 2 category and by NIST CSF function, then calculates the average effectiveness and average residual risk for each group. This reveals which areas of your compliance program are strongest and which are underperforming.

  • Now extend the main block to print gap analysis and coverage data. Add this code below the existing for loop in your if __name__ == "__main__" block:
    print("\n" + "=" * 60)
    print("GAP ANALYSIS (Effectiveness < 0.5)")
    print("=" * 60)
    gaps = detect_gaps(scored)
    if gaps.empty:
        print("No gaps detected.")
    else:
        for _, row in gaps.iterrows():
            print(
                f"  {row['control_id']} | {row['control_name']} | "
                f"Effectiveness: {row['effectiveness']} | "
                f"Residual Risk: {row['residual_risk']} ({row['risk_level']})"
            )

    print("\n" + "=" * 60)
    print("FRAMEWORK COVERAGE")
    print("=" * 60)
    soc2_cov, nist_cov = compute_coverage(scored)
    print("\nSOC 2 Trust Services Criteria:")
    for _, row in soc2_cov.iterrows():
        print(
            f"  {row['soc2_category']:<25} "
            f"Avg Effectiveness: {row['avg_effectiveness']:.2f} | "
            f"Avg Risk: {row['avg_residual_risk']:.2f} | "
            f"Controls: {row['control_count']}"
        )
    print("\nNIST CSF 2.0 Functions:")
    for _, row in nist_cov.iterrows():
        print(
            f"  {row['nist_csf_function']:<25} "
            f"Avg Effectiveness: {row['avg_effectiveness']:.2f} | "
            f"Avg Risk: {row['avg_residual_risk']:.2f} | "
            f"Controls: {row['control_count']}"
        )

What does this code do?

  • The gap analysis section lists every control with effectiveness below 50%, sorted by risk. These are the controls that need immediate remediation.
  • The framework coverage section shows how well each SOC 2 category and NIST CSF function is performing on average. A low average effectiveness signals a systemic weakness in that area.
  • Save src/risk_engine.py.

Before you run this, predict: how many controls do you think will appear in the gap analysis section?

  • Run the engine again:
python src/risk_engine.py

You should now see three sections: the scored controls table, a gap analysis listing 4 controls with effectiveness below 0.5, and framework coverage grouped by SOC 2 category and NIST CSF function.

The data is all there and correct. But notice how hard it is to spot patterns in this wall of text. Presenting this raw output to a CISO or hiring manager would not communicate your risk posture effectively.

Seeing a NameError for detect_gaps or compute_coverage?

  • Make sure you placed the new functions above the if __name__ == "__main__" block. Python reads top to bottom, so functions must be defined before they are called.
  • Check that detect_gaps and compute_coverage are not indented inside another function. They should be at the top level of the file.

Need help debugging? Ask here.

✔️ Awesome, I've got everything!

Great. Double check that you have saved both data/controls.csv and src/risk_engine.py.

ⓧ I'd like to double check the full code

Your data/controls.csv should contain this exact content:

control_id,control_name,soc2_category,nist_csf_function,impact,likelihood,effectiveness
CTRL-001,MFA Enforcement,Security,Protect,5,3,0.85
CTRL-002,Data Encryption at Rest,Confidentiality,Protect,5,4,0.90
CTRL-003,Incident Response Plan,Security,Respond,4,3,0.70
CTRL-004,Backup Recovery Testing,Availability,Recover,5,4,0.75
CTRL-005,Access Review Quarterly,Security,Govern,4,3,0.40
CTRL-006,Vulnerability Scanning,Security,Detect,4,4,0.80
CTRL-007,Change Management Process,Processing Integrity,Govern,3,3,0.65
CTRL-008,Data Retention Policy,Privacy,Identify,3,2,0.30
CTRL-009,Network Segmentation,Security,Protect,5,5,0.60
CTRL-010,Security Awareness Training,Security,Govern,3,4,0.45
CTRL-011,Log Monitoring and Alerting,Security,Detect,4,4,0.35
CTRL-012,Disaster Recovery Plan,Availability,Recover,5,3,0.80

Your src/risk_engine.py is longer than 30 lines. Scroll up and confirm these sections are present in this order:

  • Imports (pandas and Path) plus DATA_DIR and GAP_THRESHOLD constants.
  • load_controls() function.
  • compute_residual_risk() function.
  • classify_risk() function.
  • detect_gaps() function.
  • compute_coverage() function.
  • if __name__ == "__main__" block printing scored results, gap analysis, and framework coverage.

Your risk engine is producing accurate, quantitative compliance data. Next up, you will transform this wall of terminal text into an interactive visual dashboard that a CISO can actually explore.

Add Gap Detection and Coverage Analysis

Your risk engine computes a residual risk score for every control. But individual scores alone don't answer the questions a CISO needs answered: which controls are actively failing, and which compliance framework areas have the weakest coverage?

In this step, you'll put the detect_gaps() and compute_coverage() functions to work by wiring their output into your engine's main report. The data will be correct, but you'll quickly see why terminal text can't do the job of communicating risk posture to stakeholders.

In this step, get ready to:
  • Print a gap analysis showing controls below the effectiveness threshold.
  • Print framework coverage grouped by SOC 2 category and NIST CSF function.
  • Observe the limitation of terminal-only reporting for executive communication.
Add gap analysis output to your report

A control gap is any control whose effectiveness falls below a threshold. Your detect_gaps() function already filters and sorts these. Now you need to surface them in the terminal report.

  • In src/risk_engine.py, scroll to the bottom of the if __name__ == "__main__": block.
  • Find the last line of the scored controls loop (the one that prints each control's risk level).
  • Add the following gap analysis section directly below it:
    print("\n" + "=" * 60)
    print("GAP ANALYSIS (Effectiveness < 0.5)")
    print("=" * 60)
    gaps = detect_gaps(scored)
    if gaps.empty:
        print("No gaps detected.")
    else:
        for _, row in gaps.iterrows():
            print(
                f"  {row['control_id']} | {row['control_name']} | "
                f"Effectiveness: {row['effectiveness']} | "
                f"Residual Risk: {row['residual_risk']} ({row['risk_level']})"
            )

What does this code do?

This calls detect_gaps(scored) which filters for controls with effectiveness below 0.5 (the GAP_THRESHOLD constant). It returns them sorted by residual risk descending, so the most dangerous gaps appear first.

Each gap line shows the control ID, name, its current effectiveness score, and the resulting residual risk with its level classification.

  • Save src/risk_engine.py.
  • Run the engine to confirm the gap analysis appears by running this command:
python src/risk_engine.py

After the scored controls table, you should see a new "GAP ANALYSIS" section listing controls like CTRL-011 (Log Monitoring and Alerting) and CTRL-008 (Data Retention Policy) with their low effectiveness scores.

Not seeing the gap analysis section?

  • Make sure the new code is indented inside the if __name__ block (4 spaces). Python is whitespace-sensitive.
  • Check that you placed the code AFTER the scored controls loop, not inside it.
  • Verify the function name is exactly detect_gaps (not detect_gap or find_gaps).

Why isn't my gap analysis printing after the scored controls?

Add framework coverage output

Gap detection tells you which individual controls are weak. Framework coverage tells you which entire categories of your compliance program are underperforming.

  • In src/risk_engine.py, add the following framework coverage section directly below the gap analysis code you just added:
    print("\n" + "=" * 60)
    print("FRAMEWORK COVERAGE")
    print("=" * 60)
    soc2_cov, nist_cov = compute_coverage(scored)
    print("\nSOC 2 Trust Services Criteria:")
    for _, row in soc2_cov.iterrows():
        print(
            f"  {row['soc2_category']:<25} "
            f"Avg Effectiveness: {row['avg_effectiveness']:.2f} | "
            f"Avg Risk: {row['avg_residual_risk']:.2f} | "
            f"Controls: {row['control_count']}"
        )
    print("\nNIST CSF 2.0 Functions:")
    for _, row in nist_cov.iterrows():
        print(
            f"  {row['nist_csf_function']:<25} "
            f"Avg Effectiveness: {row['avg_effectiveness']:.2f} | "
            f"Avg Risk: {row['avg_residual_risk']:.2f} | "
            f"Controls: {row['control_count']}"
        )

What does this code do?

This calls compute_coverage(scored) which groups controls by soc2_category and nist_csf_function, then calculates the average effectiveness and average residual risk for each group.

The SOC 2 section shows how well each Trust Services Criteria category (Security, Availability, etc.) is covered. The NIST CSF 2.0 section shows coverage across the six functions (Govern, Identify, Protect, Detect, Respond, Recover).

  • Save src/risk_engine.py.

Before you run this, think about what it would be like to hand this terminal output to a CISO or auditor. Do you think they could quickly spot the riskiest framework area from a wall of text?

  • Run the full report by running this command:
python src/risk_engine.py

You should see all three sections: the scored controls table, the gap analysis listing controls with effectiveness below 0.5, and the framework coverage showing averages by SOC 2 category and NIST CSF function.

The data is correct, but the format is the problem

Scroll through the output. The numbers are accurate. You can see that Security has 6 controls, that Log Monitoring has the highest residual risk among gaps, and that Privacy's average effectiveness is low.

But could you spot the riskiest area in 3 seconds? Could a hiring manager or CISO glance at this and know where to invest? Terminal text forces the reader to parse every line. This is exactly why the next step transforms this data into an interactive visual dashboard.

Seeing a KeyError or missing column?

  • A KeyError: 'residual_risk' means the coverage section is trying to use the residual_risk column before it was computed. Make sure you pass scored (the output of compute_residual_risk()) to both detect_gaps() and compute_coverage().
  • If you see NameError: name 'compute_coverage' is not defined, confirm the function definition exists above the if __name__ block.

My framework coverage section is throwing an error when I run risk_engine.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved src/risk_engine.py and that running it produces all three sections in the terminal.

ⓧ I'd like to double check the full code

Your src/risk_engine.py is long, so scroll through and confirm these sections exist in order:

  • Imports and constants (DATA_DIR, GAP_THRESHOLD).
  • The load_controls() function.
  • The compute_residual_risk() function.
  • The classify_risk() function.
  • The detect_gaps() function.
  • The compute_coverage() function.
  • The if __name__ == "__main__": block printing (1) scored controls, (2) gap analysis, and (3) framework coverage.

Your engine now produces a complete risk report with gap detection and framework coverage analysis. The data is sound. But as you just saw, a wall of numbers in a terminal isn't something you can hand to a stakeholder. Next up, you'll transform this exact data into an interactive visual dashboard that communicates risk posture at a glance.

Generate the Interactive Dashboard

Your risk engine now produces correct scores, gap analysis, and coverage data. The problem is that the output is a wall of terminal text.

A CISO reviewing your compliance posture needs to see risk patterns at a glance, not scroll through rows of numbers. In this step, you will transform the same data into an interactive Plotly dashboard that anyone can explore in a browser.

In this step, get ready to:
  • Create a dashboard generator script that imports your existing scoring functions.
  • Build interactive Plotly figures for risk heatmaps and framework coverage.
  • Generate a self-contained HTML dashboard and verify it in the browser.
Create the dashboard generator with the risk heatmap function

The heatmap plots every control on an Impact vs Likelihood grid, colored by residual risk. This is the most powerful visual in the dashboard because it shows risk clusters instantly.

  • Create a new file called src/generate_dashboard.py and add the imports and output directory constant by pasting this code:
import sys
from pathlib import Path
from datetime import datetime, timezone

import plotly.graph_objects as go
import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parent))
from risk_engine import load_controls, compute_residual_risk, detect_gaps, compute_coverage


DIST_DIR = Path(__file__).resolve().parent.parent / "dist"

What does this code do?

The sys.path.insert line ensures Python can find risk_engine.py in the same directory. Then you import the four scoring functions you already built.

DIST_DIR points to a dist/ folder where the final HTML file will be written.

  • Save src/generate_dashboard.py and confirm the imports work by running:
python src/generate_dashboard.py

You should see no output and no errors. The file runs the imports and exits cleanly.

Seeing a ModuleNotFoundError?

Make sure your terminal is in the grc-risk-engine project root folder, not inside src/. Run cd ~/Desktop/grc-risk-engine and try again.

help me fix a ModuleNotFoundError when running generate_dashboard.py

  • Now start building the create_risk_heatmap function. Add this code below DIST_DIR to set up the heatmap's data matrix:
def create_risk_heatmap(df):
    # Labels for each axis of the 5x5 risk matrix
    impact_labels = ["1 - Negligible", "2 - Minor", "3 - Moderate", "4 - Major", "5 - Severe"]
    likelihood_labels = ["1 - Rare", "2 - Unlikely", "3 - Possible", "4 - Likely", "5 - Almost Certain"]

    # Build a 5x5 grid to hold average residual risk per cell
    matrix = pd.DataFrame(0.0, index=range(1, 6), columns=range(1, 6))
    counts = pd.DataFrame(0, index=range(1, 6), columns=range(1, 6))

    for _, row in df.iterrows():
        i, l = int(row["impact"]), int(row["likelihood"])
        matrix.loc[i, l] += row["residual_risk"]
        counts.loc[i, l] += 1

    counts_safe = counts.replace(0, 1)
    matrix = matrix / counts_safe
    matrix = matrix.round(2)

    # Create hover annotations showing risk value and control count
    text_matrix = []
    for i in range(1, 6):
        row_text = []
        for l in range(1, 6):
            val = matrix.loc[i, l]
            n = counts.loc[i, l]
            if n > 0:
                row_text.append(f"{val:.1f}<br>({n} ctrl)")
            else:
                row_text.append("")
        text_matrix.append(row_text)

What does this code do?

This builds a 5x5 matrix mapping each control's impact and likelihood to its average residual risk. The text_matrix provides hover annotations so viewers can see the exact risk value and how many controls fall in each cell.

  • Complete the create_risk_heatmap function by adding the Plotly figure creation directly below what you just wrote:
    fig = go.Figure(
        data=go.Heatmap(
            z=matrix.values.tolist(),
            x=likelihood_labels,
            y=impact_labels,
            colorscale=[
                [0.0, "#2ecc71"],
                [0.33, "#f1c40f"],
                [0.66, "#e67e22"],
                [1.0, "#e74c3c"],
            ],
            text=text_matrix,
            texttemplate="%{text}",
            colorbar=dict(title="Avg Residual Risk"),
        )
    )
    fig.update_layout(
        title="Risk Heatmap: Impact vs Likelihood",
        xaxis_title="Likelihood",
        yaxis_title="Impact",
        height=450,
    )
    return fig

What does this code do?

The go.Heatmap creates a color-coded grid ranging from green (low risk) through yellow and orange to red (high risk). The texttemplate makes each cell display its annotation directly on the chart.

  • Save the file and verify the function loads by running:
python -c "import sys; sys.path.insert(0, 'src'); from generate_dashboard import create_risk_heatmap; print('Heatmap function loaded')"

You should see Heatmap function loaded printed to the terminal.

Add the coverage charts and gap analysis table

The coverage charts show average control effectiveness grouped by SOC 2 category and NIST CSF 2.0 function. The gap table lists every control that falls below the effectiveness threshold.

  • Add the first half of the create_coverage_charts function below create_risk_heatmap. This builds the SOC 2 bar chart:
def create_coverage_charts(soc2_cov, nist_cov):
    # SOC 2 horizontal bar chart color-coded by effectiveness
    fig_soc2 = go.Figure(
        data=go.Bar(
            x=soc2_cov["avg_effectiveness"],
            y=soc2_cov["soc2_category"],
            orientation="h",
            marker_color=soc2_cov["avg_effectiveness"].apply(
                lambda v: "#2ecc71" if v >= 0.7 else "#e67e22" if v >= 0.5 else "#e74c3c"
            ),
            text=soc2_cov["avg_effectiveness"].apply(lambda v: f"{v:.0%}"),
            textposition="outside",
        )
    )
    fig_soc2.update_layout(
        title="SOC 2 Trust Services Criteria - Avg Control Effectiveness",
        xaxis_title="Average Effectiveness",
        xaxis=dict(range=[0, 1.1]),
        height=300,
        margin=dict(l=180),
    )
  • Directly below, add the NIST CSF 2.0 bar chart and the return statement to complete the function:
    # NIST CSF 2.0 horizontal bar chart
    fig_nist = go.Figure(
        data=go.Bar(
            x=nist_cov["avg_effectiveness"],
            y=nist_cov["nist_csf_function"],
            orientation="h",
            marker_color=nist_cov["avg_effectiveness"].apply(
                lambda v: "#2ecc71" if v >= 0.7 else "#e67e22" if v >= 0.5 else "#e74c3c"
            ),
            text=nist_cov["avg_effectiveness"].apply(lambda v: f"{v:.0%}"),
            textposition="outside",
        )
    )
    fig_nist.update_layout(
        title="NIST CSF 2.0 Functions - Avg Control Effectiveness",
        xaxis_title="Average Effectiveness",
        xaxis=dict(range=[0, 1.1]),
        height=350,
        margin=dict(l=180),
    )
    return fig_soc2, fig_nist

What does this code do?

Both charts use the same pattern: horizontal bars colored green (70%+ effectiveness), orange (50-69%), or red (below 50%). This gives an instant read on which framework areas are strong and which need attention.

  • Save and verify the coverage function loads by running:
python -c "import sys; sys.path.insert(0, 'src'); from generate_dashboard import create_coverage_charts; print('Coverage charts loaded')"

You should see Coverage charts loaded with no errors.

  • Now add the create_gap_table_html function below create_coverage_charts. This builds the row markup for the gap table:
def create_gap_table_html(gaps):
    if gaps.empty:
        return "<p>No control gaps detected. All controls meet the effectiveness threshold.</p>"

    risk_colors = {
        "Critical": "#e74c3c",
        "High": "#e67e22",
        "Medium": "#f1c40f",
        "Low": "#2ecc71",
    }

    rows = ""
    for _, row in gaps.iterrows():
        color = risk_colors.get(row["risk_level"], "#999")
        rows += f"""<tr>
            <td>{row['control_id']}</td>
            <td>{row['control_name']}</td>
            <td>{row['soc2_category']}</td>
            <td>{row['nist_csf_function']}</td>
            <td>{row['effectiveness']:.0%}</td>
            <td>{row['residual_risk']}</td>
            <td style="color: {color}; font-weight: bold;">{row['risk_level']}</td>
        </tr>"""
  • Directly below the for loop, add the return statement that wraps the rows in a full HTML table:
    return f"""
    <table style="width:100%; border-collapse: collapse; font-size: 14px;">
        <thead>
            <tr style="background: #2c3e50; color: white;">
                <th style="padding: 8px;">ID</th>
                <th style="padding: 8px;">Control</th>
                <th style="padding: 8px;">SOC 2</th>
                <th style="padding: 8px;">NIST CSF</th>
                <th style="padding: 8px;">Effectiveness</th>
                <th style="padding: 8px;">Residual Risk</th>
                <th style="padding: 8px;">Level</th>
            </tr>
        </thead>
        <tbody>{rows}</tbody>
    </table>"""

What does this code do?

This function builds a styled HTML table showing every control that falls below the 50% effectiveness threshold. Each row's risk level is color-coded so critical gaps jump out visually.

  • Save and confirm all three visualization functions load:
python -c "import sys; sys.path.insert(0, 'src'); from generate_dashboard import create_gap_table_html; print('All chart functions ready')"

You should see All chart functions ready printed cleanly.

Build the dashboard assembly function and generate the HTML

The final function ties everything together. It runs the scoring engine, creates all figures, and assembles them into one self-contained HTML page with summary metrics and a methodology section.

  • Add the generate_dashboard function below create_gap_table_html. Start with the data loading and metrics calculation:
def generate_dashboard():
    # Load and score all controls
    controls = load_controls()
    scored = compute_residual_risk(controls)
    gaps = detect_gaps(scored)
    soc2_cov, nist_cov = compute_coverage(scored)

    # Generate all visualization components
    heatmap = create_risk_heatmap(scored)
    fig_soc2, fig_nist = create_coverage_charts(soc2_cov, nist_cov)
    gap_table = create_gap_table_html(gaps)

    timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")

    # Calculate summary metrics for the dashboard header
    total_controls = len(scored)
    critical_count = len(scored[scored["risk_level"] == "Critical"])
    high_count = len(scored[scored["risk_level"] == "High"])
    gap_count = len(gaps)
    avg_effectiveness = scored["effectiveness"].mean()

What does this code do?

This section reuses your existing scoring functions to compute everything the dashboard needs. The five metric variables will populate the summary cards at the top of the page.

  • Directly below, add the HTML template that assembles all components into a full page. This first section defines the page structure and styles:
    html = f"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GRC Risk Scoring Dashboard</title>
    <style>
        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; background: #f5f6fa; color: #2c3e50; }}
        .header {{ background: #2c3e50; color: white; padding: 30px; border-radius: 8px; margin-bottom: 20px; }}
        .header h1 {{ margin: 0 0 8px 0; }}
        .header p {{ margin: 0; opacity: 0.8; }}
        .metrics {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; margin-bottom: 20px; }}
        .metric {{ background: white; padding: 20px; border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
        .metric .value {{ font-size: 2em; font-weight: bold; }}
        .metric .label {{ font-size: 0.85em; color: #7f8c8d; margin-top: 4px; }}
        .section {{ background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
        .section h2 {{ margin-top: 0; color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 8px; }}
        table td, table th {{ padding: 8px; text-align: left; border-bottom: 1px solid #ecf0f1; }}
        .methodology {{ background: #ecf0f1; padding: 15px; border-radius: 6px; font-size: 0.9em; }}
    </style>
</head>
<body>
  • Continue the HTML template directly below. Add the dashboard header, metrics cards, and chart sections:
    <div class="header">
        <h1>GRC Risk Scoring Dashboard</h1>
        <p>Automated compliance analytics | SOC 2 + NIST CSF 2.0 | Last generated: {timestamp}</p>
    </div>

    <div class="metrics">
        <div class="metric"><div class="value">{total_controls}</div><div class="label">Total Controls</div></div>
        <div class="metric"><div class="value" style="color: #e74c3c;">{critical_count}</div><div class="label">Critical Risk</div></div>
        <div class="metric"><div class="value" style="color: #e67e22;">{high_count}</div><div class="label">High Risk</div></div>
        <div class="metric"><div class="value" style="color: #e74c3c;">{gap_count}</div><div class="label">Control Gaps</div></div>
        <div class="metric"><div class="value">{avg_effectiveness:.0%}</div><div class="label">Avg Effectiveness</div></div>
    </div>

    <div class="section">
        <h2>Risk Heatmap</h2>
        {heatmap.to_html(full_html=False, include_plotlyjs='cdn')}
    </div>

    <div class="section">
        <h2>Framework Coverage</h2>
        {fig_soc2.to_html(full_html=False, include_plotlyjs=False)}
        {fig_nist.to_html(full_html=False, include_plotlyjs=False)}
    </div>

Why include_plotlyjs='cdn' on only the first chart?

The first chart loads the Plotly.js library from a CDN. Subsequent charts set include_plotlyjs=False because the library is already available on the page. This keeps the HTML file small while maintaining full interactivity.

  • Add the final HTML sections for gap analysis, methodology, and close the template:
    <div class="section">
        <h2>Gap Analysis (Effectiveness &lt; 50%)</h2>
        {gap_table}
    </div>

    <div class="section">
        <h2>Methodology</h2>
        <div class="methodology">
            <p><strong>Risk Scoring Formula:</strong> Residual Risk = Impact (1-5) x Likelihood (1-5) x (1 - Control Effectiveness)</p>
            <p><strong>Risk Levels:</strong> Critical (&gt;12) | High (&gt;8) | Medium (&gt;4) | Low (&lt;=4)</p>
            <p><strong>Gap Threshold:</strong> Controls with effectiveness below 50% are flagged for remediation.</p>
            <p><strong>DMAIC Framework:</strong> Define (control catalog) | Measure (scoring engine) | Analyze (gap detection) | Improve (priority recommendations) | Control (CI/CD auto-deployment)</p>
            <p><strong>Frameworks:</strong> SOC 2 Trust Services Criteria (2017, revised 2022) | NIST Cybersecurity Framework 2.0</p>
        </div>
    </div>
</body>
</html>"""
  • Finally, add the file-writing logic and the entry point below the closing triple-quote:
    DIST_DIR.mkdir(parents=True, exist_ok=True)
    output_path = DIST_DIR / "index.html"
    output_path.write_text(html, encoding="utf-8")
    print(f"Dashboard generated: {output_path}")


if __name__ == "__main__":
    generate_dashboard()

What does this code do?

DIST_DIR.mkdir(parents=True, exist_ok=True) creates the dist/ folder if it does not already exist. The script then writes the assembled HTML to dist/index.html and prints a confirmation message.

Before you run this, what do you think will happen? Will the script produce a file, or will there be an error from assembling all those components?

  • Save src/generate_dashboard.py and generate the dashboard by running:
python src/generate_dashboard.py

You should see Dashboard generated: followed by the path to dist/index.html.

  • Confirm the file was created by running:
ls dist/

You should see index.html listed in the output.

  • Open dist/index.html in your browser by running:
open dist/index.html

Windows users

On Windows, use start dist/index.html instead of open.

You should see a fully styled dashboard with a dark header, five metric cards, an interactive risk heatmap, two coverage bar charts, and a gap analysis table with color-coded risk levels. Hover over the heatmap cells to see control counts and risk scores.

Dashboard not rendering charts?

Make sure you have an internet connection. The heatmap loads Plotly.js from a CDN, so an offline browser will show blank chart areas.

If you see a Python traceback instead of the file being created, check that your generate_dashboard.py has no indentation errors. Every line inside generate_dashboard() must be indented with 4 spaces.

help me debug my generate_dashboard.py script

✔️ Awesome, I've got everything!

Great. Confirm you have saved src/generate_dashboard.py and that dist/index.html opens in your browser with interactive charts.

ⓧ I'd like to double check the full code

Your src/generate_dashboard.py file is long. Scroll through it and confirm these sections appear in order:

  • Imports (sys, Path, datetime, plotly.graph_objects, pandas) and the from risk_engine import line.
  • DIST_DIR constant pointing to the dist/ directory.
  • create_risk_heatmap(df) function returning a Plotly Figure.
  • create_coverage_charts(soc2_cov, nist_cov) function returning two figures.
  • create_gap_table_html(gaps) function returning an HTML string.
  • generate_dashboard() function assembling the full HTML page and writing to dist/index.html.
  • if __name__ == "__main__": block calling generate_dashboard().

Your terminal output is now an interactive visual dashboard. Next up, you will deploy this to GitHub Pages so anyone can explore it from a public URL.

Deploy to GitHub Pages with GitHub Actions

Your risk engine produces a correct, interactive dashboard locally. A hiring manager cannot see it unless you share the file directly.

Deploying it with CI/CD proves you can engineer continuous compliance infrastructure that auto-updates when control data changes. This is the "Control" phase of DMAIC. Every push to main regenerates and redeploys the dashboard automatically.

In this step, get ready to:
  • Create a GitHub Actions workflow that builds and deploys the dashboard.
  • Enable GitHub Pages in your repository settings.
  • Push your code and verify the live deployment.
Create the workflow configuration file

GitHub Actions workflows live in a special directory inside your repository. The workflow file defines when the pipeline runs and what permissions it needs.

  • Create the workflow directory by running these commands:
mkdir -p .github/workflows
  • Confirm the directory exists by running:
ls .github/workflows

You should see an empty directory listed with no errors.

  • Create the file .github/workflows/deploy.yml and paste the following workflow configuration:
name: Deploy GRC Dashboard

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

What does this configuration do?

  • on: push: branches: - main triggers the workflow every time you push to the main branch.
  • workflow_dispatch lets you trigger the workflow manually from the Actions tab.
  • permissions grants the workflow the specific rights it needs to read your code and write to GitHub Pages.
  • concurrency prevents multiple deployments from running at the same time.
  • Now add the job definition below the concurrency block in the same file:
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Generate dashboard
        run: python src/generate_dashboard.py

What do these build steps do?

  • actions/checkout@v6 clones your repository code into the runner.
  • actions/setup-python@v5 installs Python 3.11 on the runner.
  • pip install -r requirements.txt installs pandas and plotly on the runner.
  • python src/generate_dashboard.py runs your dashboard generator to produce dist/index.html fresh from the latest control data.
  • Add the deployment steps below the "Generate dashboard" step in the same file:
      - name: Setup Pages
        uses: actions/configure-pages@v5

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: dist

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

What do these deployment steps do?

  • actions/configure-pages@v5 configures the Pages environment and collects metadata.
  • actions/upload-pages-artifact@v4 packages the dist/ folder as a deployable artifact.
  • actions/deploy-pages@v4 pushes that artifact live to your GitHub Pages URL.
  • Save .github/workflows/deploy.yml.

YAML indentation errors?

YAML is whitespace-sensitive. Each nested level must use exactly 2 spaces (not tabs). If your editor inserts tabs, convert them to spaces. The steps: key must align under build-and-deploy: with 4 spaces of indent.

help me fix my deploy.yml indentation

✔️ Awesome, I've got everything!

Great. Double check you have saved the file.

ⓧ I'd like to double check the full code

Here is the complete .github/workflows/deploy.yml file:

name: Deploy GRC Dashboard

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Generate dashboard
        run: python src/generate_dashboard.py

      - name: Setup Pages
        uses: actions/configure-pages@v5

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: dist

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4
Enable GitHub Pages and push your code

Your workflow file tells GitHub Actions what to do. You also need to tell GitHub Pages to accept deployments from Actions rather than a branch.

  • Open your grc-risk-engine repository on GitHub in your browser.
  • Click the Settings tab at the top of the repository page.
  • In the left sidebar under "Code and automation", click Pages.
  • Under Build and deployment, find the Source dropdown.
  • Select GitHub Actions from the dropdown.

Why GitHub Actions as the source?

This tells GitHub Pages to accept deployments from your workflow rather than building from a branch directly. Your workflow handles the build step (running generate_dashboard.py) before uploading the output.

GitHub may suggest workflow templates after you select this option. You can ignore them because you already have your own workflow file.

  • Switch back to your terminal and stage all files by running:
git add -A
  • Commit your changes by running:
git commit -m "Add GitHub Actions deploy workflow"
  • Push to GitHub by running:
git push origin main

The push triggers your workflow automatically.

Push rejected or authentication error?

If git push asks for credentials, you may need to authenticate with a Personal Access Token or SSH key. Check that your remote URL is correct by running git remote -v and confirming it points to your grc-risk-engine repository.

help me fix my git push authentication

Verify the live deployment

Your push triggered the workflow. Now you will check that it completed successfully and your dashboard is live on the internet.

  • In your browser, navigate to your grc-risk-engine repository on GitHub.
  • Click the Actions tab at the top of the repository page.
  • You should see a workflow run called "Deploy GRC Dashboard" in progress or completed.

Before you click into the run, predict: do you think this first deployment will succeed or fail?

  • Click on the workflow run to see its details.

You should see a green checkmark next to the build-and-deploy job. This means GitHub Actions successfully installed Python, ran your dashboard generator, and deployed the output to Pages.

  • Navigate to your live dashboard by visiting:
https://[[GITHUB_USERNAME="your-github-username"]].github.io/grc-risk-engine/
  • Verify that the live dashboard shows the same risk heatmap, framework coverage charts, and gap analysis table you saw locally.

What if the page shows a 404?

GitHub Pages deployments can take 1-2 minutes after the workflow completes. Wait a moment and refresh. If it persists after 5 minutes, go back to Settings > Pages and confirm the source is set to GitHub Actions.

Workflow failed with a red X?

Click into the failed job to see the error log. Common causes include YAML indentation errors in deploy.yml or a missing requirements.txt in the repository root. Confirm all files are committed by running git status locally.

help me debug my failed GitHub Actions workflow

Your GRC risk engine is now live on the internet. Anyone with the URL can explore your interactive risk heatmap, framework coverage, and gap analysis. Next up, you will extend this platform with vendor risk assessment in the Secret Mission.

Secret mission

Add Vendor Risk Assessment

Your risk engine scores internal controls, but real-world GRC programs must also assess third-party vendor risk. Can you design a vendor data model, compute composite vendor risk scores, and add a new section to your live dashboard that visualizes vendor risk tiers and assessment freshness?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on free services. Your GitHub Pages site costs nothing to host, and the GitHub Actions workflow only consumes minutes when you push changes. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • GitHub repository grc-risk-engine (includes source code, workflow, and Pages deployment).
  • GitHub Pages site at your-username.github.io/grc-risk-engine (hosted from the repository).
  • GitHub Actions workflow (free for public repositories, 2,000 minutes/month for private repositories).
  • Local grc-risk-engine/ project folder on your Desktop.
  • Python packages pandas and plotly installed via pip.

Keep everything running

No action needed. Choose this if you plan to keep iterating on your risk engine or want the live dashboard available as a portfolio piece.

  • Your GitHub Pages site stays live at no cost.
  • The Actions workflow only runs when you push changes, so it consumes zero minutes while idle.
  • Your local project folder and installed Python packages remain on your machine.

Pause - I'll come back to this later

There is nothing to pause for this project. GitHub Pages remains live but does not incur charges, and Actions only runs on push.

  • Your live dashboard stays accessible at its public URL.
  • No processes are running locally, so there is nothing to stop.

Delete - I don't want to use this again

Remove all project resources and start fresh. Deleting the GitHub repository also removes the Pages site and the Actions workflow.

  • Open your grc-risk-engine repository on GitHub.
  • Click Settings in the repository navigation bar.
  • Scroll down to the Danger Zone section on the General settings page.
  • Click Delete this repository.
  • Click I want to delete this repository, then click I have read and understand these effects.
  • Type the repository name to confirm, then click Delete this repository.

Delete the local project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/grc-risk-engine

Windows

rmdir /s /q "$HOME\Desktop\grc-risk-engine"

Uninstall the Python packages by running:

pip uninstall pandas plotly -y

Nice Work!

Nice Work!

You just built a production-grade GRC risk scoring engine with a live interactive dashboard that auto-deploys on every push. This is the kind of compliance engineering infrastructure that proves you can build systems, not just document policies.

You've learned how to:

  • Build a Python risk scoring engine that computes residual risk using Impact x Likelihood x (1 - Effectiveness) and maps controls to SOC 2 Trust Services Criteria and NIST CSF 2.0.
  • Generate an interactive Plotly dashboard with risk heatmaps, framework coverage charts, and gap analysis tables that hiring managers can explore from a single URL.
  • Deploy continuous compliance infrastructure using GitHub Actions CI/CD to GitHub Pages, proving the DMAIC methodology's Control phase in action.
  • Secret Mission: Extended the platform with vendor risk assessment scoring and a dedicated vendor risk visualization section on the dashboard.

Ready to quiz yourself?