Skip to content

Commit b9fd09f

Browse files
author
Vadim
committed
Add examples: SpoOperation async ops, Graph subscriptions, security alerts, terms of use
1 parent a6a4326 commit b9fd09f

6 files changed

Lines changed: 398 additions & 0 deletions

File tree

examples/entraid/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ tenant-level policies, and audit logs.
151151
| **Access reviews** — history definitions, instances, decisions, compliance reporting | [`governance/access_reviews.py`](./governance/access_reviews.py) | `AccessReview.Read.All` | [access reviews](https://learn.microsoft.com/en-us/graph/api/resources/accessreviewsv2-overview) |
152152
| **Domain management** — add, verify DNS, check service records | [`domains/manage.py`](./domains/manage.py) | `Domain.ReadWrite.All` | [domains API](https://learn.microsoft.com/en-us/graph/api/resources/domain) |
153153
| **Auth methods** — tenant MFA/passwordless readiness, per-user methods (FIDO2, phone, Authenticator) | [`users/auth_methods.py`](./users/auth_methods.py) | `UserAuthenticationMethod.Read.All`, `AuditLog.Read.All` | [auth methods overview](https://learn.microsoft.com/en-us/graph/api/resources/authenticationmethods-overview) |
154+
| **Security alerts** — list, filter, triage M365 Defender alerts, update classification/status | [`security/alerts.py`](./security/alerts.py) | `SecurityAlert.ReadWrite.All` | [security alerts](https://learn.microsoft.com/en-us/graph/api/resources/alert) |
155+
| **Change notifications** — subscribe to Graph webhooks, manage subscription lifecycle | [`governance/change_notifications.py`](./governance/change_notifications.py) | Depends on resource | [subscriptions](https://learn.microsoft.com/en-us/graph/api/resources/subscription) |
156+
| **Terms of use** — list agreements, track user acceptances, compliance check | [`governance/terms_of_use.py`](./governance/terms_of_use.py) | `Agreement.Read.All` | [agreements](https://learn.microsoft.com/en-us/graph/api/resources/agreement) |
154157

155158
---
156159

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
Graph change notifications (webhook subscriptions) — subscribe to
3+
resource changes and manage subscription lifecycle.
4+
5+
Subscriptions let your application receive push notifications when
6+
Microsoft Graph resources change — users created, groups updated,
7+
messages received, files modified.
8+
9+
This example covers:
10+
- Create a subscription on a resource (e.g. users, messages)
11+
- List existing subscriptions
12+
- Renew (update expiration) a subscription
13+
- Delete a subscription
14+
- Reauthorize a subscription (for encrypted content)
15+
16+
Requires delegated permission appropriate to the resource. For
17+
``/users`` subscriptions you need ``User.Read.All``. For
18+
``/me/mailFolders('inbox')/messages`` you need ``Mail.Read``.
19+
20+
https://learn.microsoft.com/en-us/graph/api/resources/subscription
21+
"""
22+
23+
import sys
24+
from datetime import datetime, timedelta, timezone
25+
26+
from office365.graph_client import GraphClient
27+
from tests import test_client_id, test_client_secret, test_tenant
28+
29+
# Your application's notification endpoint (must be HTTPS)
30+
NOTIFICATION_URL = "https://your-app.ngrok.io/api/notifications"
31+
32+
# The resource to watch (Graph API path)
33+
# Examples:
34+
# "/users" — user create/update/delete
35+
# "/groups" — group changes
36+
# "/me/mailFolders('inbox')/messages" — new mail
37+
# "/drives/{drive-id}/root" — OneDrive file changes
38+
RESOURCE = "/users"
39+
CHANGE_TYPE = "updated,created,deleted"
40+
41+
42+
def main():
43+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
44+
45+
# -- Step 1: create a subscription --
46+
expiry = datetime.now(timezone.utc) + timedelta(hours=12)
47+
sub = (
48+
client.subscriptions.add(
49+
changeType=CHANGE_TYPE,
50+
notificationUrl=NOTIFICATION_URL,
51+
resource=RESOURCE,
52+
expirationDateTime=expiry.isoformat(),
53+
clientState="secret-123", # verify notifications in your handler
54+
)
55+
.execute_query()
56+
)
57+
print(f"Subscription created:")
58+
print(f" ID: {sub.id}")
59+
print(f" Resource: {sub.resource}")
60+
print(f" Change type: {sub.change_type}")
61+
print(f" Expiration: {sub.expiration_date_time.strftime('%Y-%m-%d %H:%M')}")
62+
if sub.latest_supported_tls_version:
63+
print(f" TLS version: {sub.latest_supported_tls_version}")
64+
print()
65+
66+
# -- Step 2: list all subscriptions --
67+
subs = client.subscriptions.get().execute_query()
68+
print(f"Active subscriptions ({len(subs)}):")
69+
for s in subs:
70+
exp = s.expiration_date_time.strftime("%Y-%m-%d %H:%M") if s.expiration_date_time else "?"
71+
print(f" {s.id:45s} resource={s.resource:30s} expires={exp}")
72+
print()
73+
74+
# -- Step 3: renew (extend expiration) --
75+
new_expiry = datetime.now(timezone.utc) + timedelta(hours=24)
76+
sub.set_property("expirationDateTime", new_expiry.isoformat())
77+
sub.update().execute_query()
78+
print(f"Subscription renewed — new expiration:")
79+
print(f" {sub.expiration_date_time.strftime('%Y-%m-%d %H:%M')}")
80+
print()
81+
82+
# -- Step 4: reauthorize (for encrypted content) --
83+
# Required when subscription includes `includeResourceData=True`
84+
# sub.reauthorize().execute_query()
85+
# print("Subscription reauthorized.")
86+
87+
# -- Step 5: delete the subscription --
88+
sub.delete_object().execute_query()
89+
print("Subscription deleted.")
90+
91+
92+
if __name__ == "__main__":
93+
main()
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Terms of use — list agreements and track user acceptances.
3+
4+
Microsoft Entra Terms of Use lets organizations present legal
5+
disclaimers or policy documents to users. This example shows how to:
6+
- List all agreements configured in the tenant
7+
- Check acceptance status per user
8+
- Track who has (and hasn't) accepted each agreement
9+
10+
Compliance and HR teams use this for onboarding audits and
11+
acceptance tracking.
12+
13+
Requires delegated permission ``Agreement.Read.All`` and
14+
``AgreementAcceptance.Read.All``.
15+
16+
https://learn.microsoft.com/en-us/graph/api/resources/agreement
17+
"""
18+
19+
from office365.graph_client import GraphClient
20+
from tests import test_client_id, test_client_secret, test_tenant
21+
22+
23+
def main():
24+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
25+
26+
tos = client.identity_governance.terms_of_use
27+
28+
# -- Step 1: list all agreements --
29+
agreements = tos.agreements.get().execute_query()
30+
print(f"Terms of use agreements: {len(agreements)}\n")
31+
32+
for a in agreements:
33+
display_name = a.display_name or "(unnamed)"
34+
required = a.is_viewing_before_acceptance_required
35+
per_device = a.is_per_device_acceptance_required
36+
reaccept = a.user_reaccept_required_frequency
37+
38+
print(f" {display_name:45s} "
39+
f"view_required={required} "
40+
f"per_device={per_device} "
41+
f"reaccept_freq={reaccept}"
42+
)
43+
44+
# Count acceptances
45+
try:
46+
acceptances = a.acceptances.get().execute_query()
47+
accepted_users = set()
48+
for acc in acceptances:
49+
if acc.properties.get("userPrincipalName"):
50+
accepted_users.add(acc.properties["userPrincipalName"])
51+
52+
print(f" Accepted by: {len(accepted_users)} user(s)")
53+
if accepted_users:
54+
for u in list(accepted_users)[:5]:
55+
displayed = u.properties.get("displayName", u.properties.get("userPrincipalName", u.id if hasattr(u, 'id') else str(u)))
56+
print(f" {u}")
57+
if len(accepted_users) > 5:
58+
print(f" … and {len(accepted_users) - 5} more")
59+
except Exception:
60+
print(f" (acceptances not accessible)")
61+
62+
# Show agreement download link
63+
try:
64+
files = a.files.get().execute_query()
65+
for f in files:
66+
file_name = f.properties.get("fileName", "?")
67+
print(f" File: {file_name}")
68+
except Exception:
69+
pass
70+
71+
print()
72+
73+
# -- Step 2: list all agreement acceptances tenant-wide --
74+
all_acceptances = tos.agreement_acceptances.get().execute_query()
75+
print(f"Total agreement acceptances across all agreements: {len(all_acceptances)}")
76+
77+
for acc in all_acceptances[:10]:
78+
agreement_id = acc.properties.get("agreementId", "?")[:20]
79+
user_name = acc.properties.get("userPrincipalName", acc.properties.get("userDisplayName", "?"))
80+
recorded = acc.properties.get("recordedDateTime", "?")
81+
if hasattr(recorded, "strftime"):
82+
recorded = recorded.strftime("%Y-%m-%d")
83+
state = acc.properties.get("state", "?")
84+
print(f" {user_name:35s} agreement={agreement_id} state={state} recorded={recorded}")
85+
86+
# -- Step 3: users who have NOT accepted (compliance check) --
87+
if agreements:
88+
print(f"\n--- Compliance check: acceptance status ---")
89+
users = client.users.get().select(["id", "userPrincipalName", "displayName"]).top(100).execute_query()
90+
accepted_upns = set()
91+
for acc in all_acceptances:
92+
upn = acc.properties.get("userPrincipalName", "")
93+
if upn:
94+
accepted_upns.add(upn.upper())
95+
96+
users_without = [u for u in users if (u.user_principal_name or "").upper() not in accepted_upns]
97+
if users_without:
98+
print(f"Users without acceptance: {len(users_without)} out of {len(users)}")
99+
for u in users_without[:5]:
100+
print(f" {u.user_principal_name:35s} {u.display_name or ''}")
101+
if len(users_without) > 5:
102+
print(f" … and {len(users_without) - 5} more")
103+
104+
105+
if __name__ == "__main__":
106+
main()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Security alerts — list, triage, and update Microsoft 365 Defender
3+
alerts programmatically.
4+
5+
Security operations teams use this to:
6+
- List active alerts across the tenant
7+
- Filter by severity, status, or detection source
8+
- Update alert classification, determination, and status
9+
- Automate triage (e.g. auto-resolve low-severity known issues)
10+
11+
Requires delegated permission ``SecurityAlert.ReadWrite.All``.
12+
13+
https://learn.microsoft.com/en-us/graph/api/resources/alert
14+
"""
15+
16+
from datetime import datetime, timedelta, timezone
17+
18+
from office365.graph_client import GraphClient
19+
from tests import test_client_id, test_client_secret, test_tenant
20+
21+
22+
def main():
23+
client = GraphClient(tenant=test_tenant).with_client_secret(test_client_id, test_client_secret)
24+
25+
alerts = client.security.alerts_v2
26+
27+
# -- Step 1: list recent active alerts --
28+
since = (datetime.now(timezone.utc) - timedelta(days=3)).isoformat()
29+
all_alerts = alerts.get().execute_query()
30+
print(f"Total alerts: {len(all_alerts)}\n")
31+
32+
# Filter by status
33+
active = [a for a in all_alerts if a.properties.get("status") in ("new", "inProgress")]
34+
resolved = [a for a in all_alerts if a.properties.get("status") == "resolved"]
35+
36+
print(f"Active: {len(active)}")
37+
print(f"Resolved: {len(resolved)}\n")
38+
39+
if not active:
40+
print("No active alerts. Showing recent alerts:\n")
41+
42+
for a in (active or all_alerts[:10]):
43+
alert_id = a.id or "?"
44+
title = a.properties.get("title", "(untitled)")
45+
severity = a.properties.get("severity", "?")
46+
status = a.properties.get("status", "?")
47+
created = a.properties.get("createdDateTime", "?")
48+
if isinstance(created, datetime):
49+
created = created.strftime("%m-%d %H:%M")
50+
51+
detection = a.properties.get("detectionSource", a.properties.get("serviceSource", "?"))
52+
53+
print(f" [{severity:7s}] [{status:10s}] {title}")
54+
print(f" id={alert_id[:20]} source={detection} created={created}")
55+
56+
# Show tags if any
57+
tags = a.properties.get("tags", [])
58+
if tags:
59+
print(f" tags: {', '.join(tags)}")
60+
print()
61+
62+
# -- Step 2: update an alert (triage) --
63+
if active:
64+
target = active[0]
65+
alert_id = target.id
66+
title = target.properties.get("title", "(untitled)")
67+
print(f"Updating alert: {title}")
68+
69+
# Available classification values:
70+
# unknown, falsePositive, truePositive, informationalExpectedActivity
71+
#
72+
# Available determination values:
73+
# unknown, apt, malware, phishing, securityPersonnel, securityTesting
74+
# unwantedSoftware, other, multiStagedAttack, compromisedAccount
75+
# maliciousUserActivity, notEnoughDataToValidate
76+
#
77+
# Available status values:
78+
# unknown, new, inProgress, resolved
79+
80+
target.set_property("classification", "informationalExpectedActivity")
81+
target.set_property("determination", "securityTesting")
82+
target.set_property("status", "inProgress")
83+
target.set_property("comment", "Reviewed — security testing, investigating")
84+
target.update().execute_query()
85+
print(f" ✓ Updated: classification/status modified for {title}")
86+
87+
# -- Step 3: create a custom alert (for security workflows) --
88+
# Alerts can also be created, for example from a SOAR playbook:
89+
#
90+
# from office365.directory.security.alerts.alert import Alert
91+
# new_alert = Alert(client.context)
92+
# new_alert.set_property("title", "Custom alert from automation")
93+
# new_alert.set_property("severity", "medium")
94+
# new_alert.set_property("status", "new")
95+
# new_alert.set_property("category", "CustomAutomation")
96+
# client.security.alerts_v2.add(new_alert)
97+
# client.execute_query()
98+
# print("Custom alert created.")
99+
100+
101+
if __name__ == "__main__":
102+
main()

examples/sharepoint/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ prints clear output.
2727
| [`lists/`](./lists/) | List CRUD, import/export, paging |
2828
| [`migration/`](./migration/) | Migration assessment scanner |
2929
| [`navigation/`](./navigation/) | Top nav, Quick Launch |
30+
| [`operations/`](./operations/) | Long-running async operations (create/update/delete sites), SpoOperation polling |
3031
| [`pages/`](./pages/) | Modern site pages CRUD, news |
3132
| [`propertybag/`](./propertybag/) | Custom key-value pairs on webs |
3233
| [`permissions/`](./permissions/) | Grant, revoke, break inheritance |

0 commit comments

Comments
 (0)