| import gradio as gr |
| import plotly.graph_objects as go |
| from datetime import datetime |
| import json |
|
|
| |
| 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 |
|
|
| |
| 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""" |
| |
| 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 |
|
|
| |
| scores, overall_score, recommendations = calculate_scores(checks_state) |
|
|
| |
| radar = create_radar_chart(scores, language) |
| gauge = create_gauge_chart(overall_score, language) |
|
|
| |
| recommendations_html = create_recommendations_html(recommendations, language) |
|
|
| |
| kql_html = create_kql_queries_html(language) |
|
|
| |
| markdown_report = generate_markdown_report(checks_state, scores, overall_score, language) |
|
|
| return radar, gauge, recommendations_html, kql_html, markdown_report |
|
|
| |
| 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 |
| ) |
|
|
| |
| 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 |
| ) |
|
|
| |
| 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() |
|
|
| |
| 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() |
|
|
| |
| demo.load( |
| fn=lambda lang: create_resources_html("FR" if lang == "FR - Francais" else "EN"), |
| inputs=[language_radio], |
| outputs=[resources_html] |
| ) |
|
|
| |
| 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() |
|
|