AYI-NEDJIMI's picture
Upload m365-scorecard Space
807b17d verified
raw
history blame contribute delete
19.6 kB
import gradio as gr
import plotly.graph_objects as go
from datetime import datetime
import json
# Security categories and checks
SECURITY_CHECKS = {
"identity_access": {
"en": "Identity & Access",
"fr": "Identite et Acces",
"checks": [
{"en": "MFA enabled for all users", "fr": "MFA activee pour tous les utilisateurs"},
{"en": "Conditional Access policies configured", "fr": "Politiques d'acces conditionnel configurees"},
{"en": "Legacy authentication blocked", "fr": "Authentification heritee bloquee"},
{"en": "Privileged account protection enabled", "fr": "Protection des comptes privilegies activee"},
{"en": "Password policy enforced (minimum 14 chars)", "fr": "Politique de mot de passe appliquee (14 caracteres min)"},
{"en": "Sign-in risk policies configured", "fr": "Politiques de risque de connexion configurees"},
{"en": "User risk policies configured", "fr": "Politiques de risque utilisateur configurees"},
{"en": "Guest access restrictions applied", "fr": "Restrictions d'acces invites appliquees"},
]
},
"data_protection": {
"en": "Data Protection",
"fr": "Protection des donnees",
"checks": [
{"en": "DLP policies enabled", "fr": "Politiques DLP activees"},
{"en": "Sensitivity labels configured", "fr": "Etiquettes de sensibilite configurees"},
{"en": "Encryption at rest enabled", "fr": "Chiffrement au repos active"},
{"en": "Encryption in transit enforced", "fr": "Chiffrement en transit applique"},
{"en": "External sharing restricted", "fr": "Partage externe restreint"},
{"en": "Data classification implemented", "fr": "Classification des donnees implementee"},
{"en": "Privileged access workstations (PAW) deployed", "fr": "Postes de travail d'acces privilege (PAW) deployes"},
{"en": "Data loss prevention monitored", "fr": "Prevention des pertes de donnees surveillee"},
]
},
"email_security": {
"en": "Email Security",
"fr": "Securite des emails",
"checks": [
{"en": "Anti-phishing policies enabled", "fr": "Politiques anti-phishing activees"},
{"en": "Anti-spam filtering configured", "fr": "Filtrage anti-spam configure"},
{"en": "Safe Links protection enabled", "fr": "Protection Safe Links activee"},
{"en": "Safe Attachments enabled", "fr": "Safe Attachments activee"},
{"en": "DMARC configured", "fr": "DMARC configure"},
{"en": "SPF records configured", "fr": "Enregistrements SPF configures"},
{"en": "DKIM enabled", "fr": "DKIM active"},
{"en": "Mail encryption enabled", "fr": "Chiffrement des emails active"},
{"en": "Malware detection enabled", "fr": "Detection des malwares activee"},
{"en": "External email tagging enabled", "fr": "Marquage des emails externes active"},
]
},
"app_security": {
"en": "Application Security",
"fr": "Securite des applications",
"checks": [
{"en": "App consent policies configured", "fr": "Politiques de consentement d'application configurees"},
{"en": "OAuth app restrictions enforced", "fr": "Restrictions d'application OAuth appliquees"},
{"en": "API permissions audited", "fr": "Permissions d'API auditees"},
{"en": "Third-party app access monitored", "fr": "Acces d'application tierce surveille"},
{"en": "Risky app detection enabled", "fr": "Detection d'application risquee activee"},
{"en": "Application credential protection enabled", "fr": "Protection des identifiants d'application activee"},
{"en": "API throttling configured", "fr": "Limitation de l'API configuree"},
{"en": "App connector security hardened", "fr": "Securite des connecteurs d'application durcie"},
]
},
"monitoring_audit": {
"en": "Monitoring & Audit",
"fr": "Surveillance et audit",
"checks": [
{"en": "Unified Audit Log enabled", "fr": "Journal d'audit unifie active"},
{"en": "Alert policies configured", "fr": "Politiques d'alerte configurees"},
{"en": "Sentinel integration enabled", "fr": "Integration Sentinel activee"},
{"en": "Advanced Audit enabled", "fr": "Audit avance active"},
{"en": "User activity monitoring enabled", "fr": "Surveillance de l'activite utilisateur activee"},
{"en": "Admin activity logging enabled", "fr": "Journalisation de l'activite administrateur activee"},
{"en": "Cloud app security configured", "fr": "Securite des applications cloud configuree"},
{"en": "Anomaly detection enabled", "fr": "Detection des anomalies activee"},
{"en": "Incident response procedures defined", "fr": "Procedures de reponse aux incidents definies"},
{"en": "Regular log review process established", "fr": "Processus d'examen regulier des journaux etabli"},
]
},
"compliance": {
"en": "Compliance",
"fr": "Conformite",
"checks": [
{"en": "Retention policies configured", "fr": "Politiques de retention configurees"},
{"en": "eDiscovery configured", "fr": "eDiscovery configuree"},
{"en": "Communication Compliance enabled", "fr": "Conformite de la communication activee"},
{"en": "Records Management configured", "fr": "Gestion des enregistrements configuree"},
{"en": "Legal hold capabilities configured", "fr": "Fonctionnalites de conservation legale configurees"},
{"en": "Information barriers configured", "fr": "Barrieres informationnelles configurees"},
{"en": "GDPR compliance controls enabled", "fr": "Controles de conformite RGPD actives"},
{"en": "Insider risk management enabled", "fr": "Gestion des risques d'initie activee"},
{"en": "Data residency requirements met", "fr": "Exigences de residencalite des donnees respectees"},
{"en": "Compliance Manager dashboards reviewed", "fr": "Tableaux de bord du gestionnaire de conformite examines"},
]
}
}
KQL_HUNTING_QUERIES = [
{
"title": "Detect MFA Bypass Attempts",
"query": "SigninLogs | where ConditionalAccessStatus == 'failure' and AuthenticationRequirement == 'multiFactorAuthentication' | summarize Count=count() by UserPrincipalName, IPAddress, ClientAppUsed"
},
{
"title": "Detect Privilege Escalation via PIM",
"query": "AuditLogs | where OperationName has 'Add member' and TargetResources[0].type == 'User' | project TimeGenerated, InitiatedBy, TargetResources, OperationName"
},
{
"title": "Detect Suspicious Mail Forwarding Rules",
"query": "MailEvents | where EventType == 'ForwardingRuleCreated' and SourceSystem == 'Exchange' | project TimeGenerated, Sender, ForwardingAddress, Subject"
},
{
"title": "Detect Mass File Access or Download",
"query": "CloudAppEvents | where ActionType == 'FileDownloaded' | summarize Count=count() by UserId, Timestamp, bin(Timestamp, 5m) | where Count > 50"
},
{
"title": "Detect Risky OAuth App Consent",
"query": "AuditLogs | where OperationName == 'Add application' and ActivityDisplayName contains 'consent' | project TimeGenerated, InitiatedBy, TargetResources, Result"
}
]
def calculate_scores(checks_state):
"""Calculate scores per category"""
scores = {}
recommendations = []
for category_id, category_data in SECURITY_CHECKS.items():
if category_id in checks_state:
checked_items = sum(checks_state[category_id])
total_items = len(category_data["checks"])
score = (checked_items / total_items) * 100 if total_items > 0 else 0
scores[category_id] = score
# Generate recommendations for unchecked items
unchecked = [check for i, check in enumerate(category_data["checks"]) if not checks_state[category_id][i]]
for check in unchecked:
recommendations.append({
"category": category_data,
"check": check,
"priority": len(recommendations) + 1
})
overall_score = sum(scores.values()) / len(scores) if scores else 0
return scores, overall_score, recommendations
def create_radar_chart(scores, language):
"""Create radar chart for category scores"""
categories_display = [SECURITY_CHECKS[cat_id][language] for cat_id in scores.keys()]
values = list(scores.values())
fig = go.Figure(data=go.Scatterpolar(
r=values,
theta=categories_display,
fill='toself',
name='Security Score',
line=dict(color='#4f46e5'),
fillcolor='rgba(79, 70, 229, 0.2)'
))
fig.update_layout(
polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
showlegend=False,
height=400,
title={"text": "Security Score by Category", "x": 0.5, "xanchor": "center"},
font=dict(size=12)
)
return fig
def create_gauge_chart(overall_score, language):
"""Create gauge chart for overall maturity"""
fig = go.Figure(go.Indicator(
mode="gauge+number+delta",
value=overall_score,
title={'text': "Overall Security Maturity Score"},
domain={'x': [0, 1], 'y': [0, 1]},
gauge={
'axis': {'range': [0, 100]},
'bar': {'color': "darkblue"},
'steps': [
{'range': [0, 25], 'color': "rgba(255, 0, 0, 0.2)"},
{'range': [25, 50], 'color': "rgba(255, 165, 0, 0.2)"},
{'range': [50, 75], 'color': "rgba(255, 255, 0, 0.2)"},
{'range': [75, 100], 'color': "rgba(0, 255, 0, 0.2)"}
],
'threshold': {
'line': {'color': "red", 'width': 4},
'thickness': 0.75,
'value': 90
}
}
))
fig.update_layout(height=400)
return fig
def create_recommendations_html(recommendations, language, limit=5):
"""Create recommendations panel"""
if not recommendations:
return "<p>All checks passed! Your M365 security posture is strong.</p>"
html = "<h3>Top 5 Priority Fixes</h3><ol>"
for i, rec in enumerate(recommendations[:limit], 1):
check_text = rec["check"][language]
category_text = rec["category"][language]
html += f"<li><strong>{category_text}:</strong> {check_text}</li>"
html += "</ol>"
return html
def create_kql_queries_html(language):
"""Create KQL hunting queries section"""
html = "<h3>M365 Threat Hunting - KQL Queries</h3>"
for i, query_data in enumerate(KQL_HUNTING_QUERIES, 1):
html += f"<h4>{i}. {query_data['title']}</h4>"
html += f"<pre style='background-color: #f0f0f0; padding: 10px; border-radius: 5px; overflow-x: auto;'>{query_data['query']}</pre>"
return html
def create_resources_html(language):
"""Create resources section with backlinks"""
resources = [
{
"title": "Top 10 Tools for Microsoft 365 Security Analysis",
"url": "https://ayinedjimi-consultants.fr/top-10-outils-analyse-securite-microsoft-365.html"
},
{
"title": "Zero Trust Implementation in Microsoft 365",
"url": "https://ayinedjimi-consultants.fr/zero-trust-microsoft-365-implementation.html"
},
{
"title": "Threat Hunting with Microsoft 365 Defender and Sentinel",
"url": "https://ayinedjimi-consultants.fr/threat-hunting-microsoft-365-defender-sentinel.html"
},
{
"title": "Secure M365 Access with Conditional Access and MFA",
"url": "https://ayinedjimi-consultants.fr/securiser-acces-microsoft-365-conditional-access-mfa.html"
},
{
"title": "Automate M365 Security Audit with PowerShell and Graph",
"url": "https://ayinedjimi-consultants.fr/automatiser-audit-securite-microsoft-365-powershell-graph.html"
},
{
"title": "Leveraging Microsoft Graph API for Audit and Monitoring",
"url": "https://ayinedjimi-consultants.fr/exploiter-api-microsoft-graph-audit-monitoring.html"
},
{
"title": "Advanced M365 Audit with Log Correlation",
"url": "https://ayinedjimi-consultants.fr/audit-avance-microsoft-365-correlation-journaux-logs.html"
},
{
"title": "M365 Security Best Practices 2025",
"url": "https://ayinedjimi-consultants.fr/meilleures-pratiques-securite-microsoft-365-2025.html"
},
{
"title": "M365 Compliance: Integrated Tools and Audit",
"url": "https://ayinedjimi-consultants.fr/microsoft-365-conformite-outils-integres-audit.html"
},
{
"title": "Detecting Compromised Identities in Azure AD",
"url": "https://ayinedjimi-consultants.fr/microsoft-365-azure-ad-detection-attaques-compromission-identites.html"
},
{
"title": "Microsoft 365 Audit Guide",
"url": "https://ayinedjimi-consultants.fr/audit-microsoft-365.html"
}
]
html = "<h3>Resources & Learning Materials</h3><ul>"
for resource in resources:
html += f"<li><a href='{resource['url']}' target='_blank'>{resource['title']}</a></li>"
html += "</ul>"
return html
def generate_markdown_report(checks_state, scores, overall_score, language):
"""Generate markdown report for export"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lang_code = "FR" if language == "fr" else "EN"
report = f"# M365 Security Scorecard Report\n\n"
report += f"**Report Generated:** {timestamp}\n"
report += f"**Language:** {lang_code}\n"
report += f"**Overall Security Score:** {overall_score:.1f}%\n\n"
report += "## Category Scores\n\n"
for category_id, score in scores.items():
cat_name = SECURITY_CHECKS[category_id][language]
report += f"- **{cat_name}:** {score:.1f}%\n"
report += "\n## Detailed Assessment\n\n"
for category_id, category_data in SECURITY_CHECKS.items():
cat_name = category_data[language]
report += f"### {cat_name}\n\n"
if category_id in checks_state:
for i, check in enumerate(category_data["checks"]):
check_text = check[language]
status = "✓ PASS" if checks_state[category_id][i] else "✗ FAIL"
report += f"- {status}: {check_text}\n"
report += "\n"
report += f"\n---\nGenerated by M365 Security Scorecard\n"
report += f"Created by [AYI-NEDJIMI Consultants](https://ayinedjimi-consultants.fr/bio.html)"
return report
def update_display(language, *check_args):
"""Update all displays based on checks"""
# Convert check_args to a dictionary matching the structure
checks_state = {}
arg_index = 0
for category_id in SECURITY_CHECKS.keys():
num_checks = len(SECURITY_CHECKS[category_id]["checks"])
checks_state[category_id] = list(check_args[arg_index:arg_index + num_checks])
arg_index += num_checks
# Calculate scores
scores, overall_score, recommendations = calculate_scores(checks_state)
# Create visualizations
radar = create_radar_chart(scores, language)
gauge = create_gauge_chart(overall_score, language)
# Create recommendations
recommendations_html = create_recommendations_html(recommendations, language)
# Create KQL section
kql_html = create_kql_queries_html(language)
# Create markdown report
markdown_report = generate_markdown_report(checks_state, scores, overall_score, language)
return radar, gauge, recommendations_html, kql_html, markdown_report
# Create the Gradio interface
with gr.Blocks(title="M365 Security Scorecard", theme=gr.themes.Soft(primary_hue="blue", secondary_hue="indigo")) as demo:
gr.Markdown("# M365 Security Scorecard - Microsoft 365 Security Assessment")
gr.Markdown("Evaluate your Microsoft 365 security posture across six key dimensions. Toggle language, complete the assessment, and receive actionable recommendations.")
with gr.Row():
language_radio = gr.Radio(
choices=["EN - English", "FR - Francais"],
value="EN - English",
label="Language / Langue",
interactive=True
)
# Create tabs for each category
with gr.Tabs():
check_components = {}
for category_id, category_data in SECURITY_CHECKS.items():
with gr.Tab(label=category_data["en"]):
gr.Markdown(f"### {category_data['en']} / {category_data['fr']}")
for i, check in enumerate(category_data["checks"]):
check_key = f"{category_id}_{i}"
check_components[check_key] = gr.Checkbox(
label=f"{check['en']} / {check['fr']}",
value=False
)
# Results tab
with gr.Tab(label="Assessment Results"):
with gr.Row():
radar_chart = gr.Plot(label="Category Scores")
gauge_chart = gr.Plot(label="Overall Score")
gr.Markdown("## Recommendations")
recommendations_html = gr.HTML()
gr.Markdown("## KQL Hunting Queries")
kql_html = gr.HTML()
# Export tab
with gr.Tab(label="Export & Resources"):
with gr.Row():
export_btn = gr.Button("Download Markdown Report", variant="primary")
markdown_output = gr.Textbox(
label="Markdown Report",
lines=20,
interactive=False,
max_lines=50
)
export_btn.click(
fn=lambda *args: update_display("EN" if args[0] == "EN - English" else "FR", *args[1:]),
inputs=[language_radio] + list(check_components.values()),
outputs=[radar_chart, gauge_chart, recommendations_html, kql_html, markdown_output]
)
gr.Markdown("## Resources & Learning Materials")
resources_html = gr.HTML()
# Update resources on load
demo.load(
fn=lambda lang: create_resources_html("FR" if lang == "FR - Francais" else "EN"),
inputs=[language_radio],
outputs=[resources_html]
)
# Change event to update results in real-time
language_radio.change(
fn=lambda *args: update_display("EN" if args[0] == "EN - English" else "FR", *args[1:]),
inputs=[language_radio] + list(check_components.values()),
outputs=[radar_chart, gauge_chart, recommendations_html, kql_html, markdown_output]
)
for component in check_components.values():
component.change(
fn=lambda *args: update_display("EN" if args[0] == "EN - English" else "FR", *args[1:]),
inputs=[language_radio] + list(check_components.values()),
outputs=[radar_chart, gauge_chart, recommendations_html, kql_html, markdown_output]
)
gr.Markdown("---")
gr.Markdown("Created by [AYI-NEDJIMI Consultants](https://ayinedjimi-consultants.fr/bio.html)")
if __name__ == "__main__":
demo.launch()