Governance, Risk, and Compliance (GRC) with LangGraph
Problem Statement
Evaluating third-party vendor risk requires reading 50-100 page SOC 2 Type II reports, vendor questionnaires, and sub-processor disclosures. Rule-based parsers break when formatting varies, while simple keyword searches miss critical context—such as whether a reported exception in control CC7.2 was mitigated by compensating controls. An agentic graph with dynamic verification loops allows autonomous analysis, exception tracking, and risk rating assignment.
Goal
Build a LangGraph state machine that ingests unstructured vendor audit summaries, extracts control exceptions, evaluates compensating controls, and auto-populates a Vendor Risk Assessment (VRA) record.
Outcome Measurement
- Parsing Thoroughness: 100% of reported control exceptions identified across a test corpus of 25 vendor audit reports.
- Review Latency: Vendor onboarding review turnaround decreased from 3 business days to under 15 minutes.
How the Code was Built
The graph features a self-correcting evaluation loop:
Parse_Report: Extracts exceptions and complementary user entity controls (CUECs).Validate_Compensating_Controls: Checks if stated vendor mitigations fulfill corporate safety requirements.Risk_Scoring: Conditional routing logic routes high-risk vendors to an explicit Human-in-the-Loop approval node before final assessment generation.
Executable Code
import osfrom typing import TypedDict, Listfrom pydantic import BaseModel, Fieldfrom langgraph.graph import StateGraph, ENDfrom langchain_openai import ChatOpenAIclass VendorException(BaseModel): control_id: str = Field(description="Control ID, e.g., CC6.1") description: str = Field(description="Description of exception") has_compensating_control: bool = Field(description="True if compensating control identified") risk_level: str = Field(description="HIGH, MEDIUM, or LOW")class VendorRiskState(TypedDict): report_text: str parsed_exceptions: List[dict] overall_risk_score: str requires_human_review: bool final_assessment: strllm = ChatOpenAI(model="gpt-4o", temperature=0)def parser_node(state: VendorRiskState) -> dict: structured_llm = llm.with_structured_output(VendorException) prompt = f"Analyze the following SOC2 report text for exceptions and controls:\n{state['report_text']}" res = structured_llm.invoke(prompt) exceptions = [res.model_dump()] return {"parsed_exceptions": exceptions}def risk_evaluator_node(state: VendorRiskState) -> dict: exceptions = state["parsed_exceptions"] high_risks = [e for e in exceptions if e.get("risk_level") == "HIGH" and not e.get("has_compensating_control")] if high_risks: return {"overall_risk_score": "HIGH", "requires_human_review": True} return {"overall_risk_score": "LOW", "requires_human_review": False}def generate_assessment_node(state: VendorRiskState) -> dict: score = state["overall_risk_score"] review = state["requires_human_review"] summary = f"Vendor Risk Rating: {score}. Requires Escalation: {review}. Total Exceptions Evaluated: {len(state['parsed_exceptions'])}" return {"final_assessment": summary}def route_risk(state: VendorRiskState) -> str: if state["requires_human_review"]: return "escalate" return "approve"def escalation_node(state: VendorRiskState) -> dict: assessment = state["final_assessment"] + " | ESCALATED: Requires CISO Sign-off due to unmitigated high risk." return {"final_assessment": assessment}workflow = StateGraph(VendorRiskState)workflow.add_node("parse", parser_node)workflow.add_node("evaluate", risk_evaluator_node)workflow.add_node("assess", generate_assessment_node)workflow.add_node("escalate", escalation_node)workflow.set_entry_point("parse")workflow.add_edge("parse", "evaluate")workflow.add_edge("evaluate", "assess")workflow.add_conditional_edges("assess", route_risk, {"escalate": "escalate", "approve": END})workflow.add_edge("escalate", END)app = workflow.compile()if __name__ == "__main__": test_report = "Control CC7.2 Exception: Production database access was granted without ticket approval in 3 instances. No compensating automated audit logs were available." res = app.invoke({"report_text": test_report, "parsed_exceptions": [], "overall_risk_score": "", "requires_human_review": False, "final_assessment": ""}) print(res["final_assessment"])






Leave a Reply