01Resolve a SharePoint site id from its hostname and pathVerified

GET /sites/{hostname}:/{server-relative-path} returns the site resource for a site you only know by URL. Every later call for its drives and lists uses the id from this response.

Approach and steps
  1. Split the site URL into the hostname (example.sharepoint.com) and the server-relative path (/sites/docs)
  2. Passing "hostname:/path" to by_site_id() in msgraph-sdk percent-encodes ":" and "/", which changes the documented URL shape (checked by printing the request URL). Build the documented URL and call it through with_url()
  3. Print the site's id, displayName and webUrl, and keep the id in the SITE_ID environment variable
  4. The least privileged permission is delegated Sites.Read.All. To limit an app to one site, use Sites.Selected (separate tip)
PythonSign-in required (Microsoft Entra)
"""Resolve a SharePoint site id from its URL (hostname + server-relative path)."""
import asyncio
import os
from urllib.parse import urlparse

from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient
from msgraph.generated.sites.item.site_item_request_builder import SiteItemRequestBuilder

SCOPES = ["Sites.Read.All"]
SITE_URL = os.environ.get("SITE_URL", "https://example.sharepoint.com/sites/docs")


def site_path_url(site_url):
    # by_site_id() percent-encodes ":" and "/", so build the documented URL shape
    u = urlparse(site_url)
    return "https://graph.microsoft.com/v1.0/sites/%s:%s" % (u.hostname, u.path.rstrip("/"))


async def main():
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    builder = SiteItemRequestBuilder(client.request_adapter, site_path_url(SITE_URL))
    site = await builder.get()
    print("id:", site.id)
    print("name:", site.display_name)
    print("webUrl:", site.web_url)


if __name__ == "__main__":
    asyncio.run(main())
Output(2026-09-12)
https://graph.microsoft.com/v1.0/sites/example.sharepoint.com:/sites/docs
Example instruction for Copilot

Using msgraph-sdk and DeviceCodeCredential, write Python that takes a SharePoint site URL, splits out the hostname and path, calls GET /sites/{hostname}:/{path}, and prints the site id and webUrl. by_site_id encodes ":", so build the URL and call it with with_url.

Caution

Write the path exactly as it appears in the URL. If the site does not exist or you lack access, Graph returns an error, so confirm you got an id before moving on. Sites.Read.All covers documents and list items in every site the signed-in user can read. Pass the id through an environment variable instead of hard-coding it.

Availability
Microsoft Graph v1.0. Least privileged permission is Sites.Read.All for both delegated (work or school account) and application. Personal Microsoft accounts are not supported. Uses msgraph-sdk and azure-identity.
Requires
msgraph-sdk 1.62.0, azure-identity 1.25.3
Permissions
Sites.Read.All (delegated)
Tested
run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
Practical edition
SharePoint / OneDrive ── Use SharePoint site agents to find internal documents
Source
Microsoft Learn, "Get a site resource by path"
Microsoft Learn, "Get a SharePoint Site"
Verified
2026-09-12 (v1)
Supporting passages from the sources
In addition to retrieving a site by ID you can retrieve a site based on server-relative URL path.
If you have the server-relative URL for a site resource, you can construct a request as follows:

02List a site's document libraries and the items in one folderVerified

GET /sites/{siteId}/drives lists a site's document libraries (drives). Pick one by name, then list a folder's children with $select so only the needed properties come back, following @odata.nextLink across pages.

Approach and steps
  1. Get the drives of the SITE_ID site and pick the drive whose name matches the library
  2. Address the folder by path: build /drives/{drive-id}/root:/{path}:/children and call it with with_url(), because by_drive_item_id() percent-encodes ":" and "/"
  3. Use $select for name, size, lastModifiedDateTime, webUrl, folder and file to keep responses small
  4. While odata_next_link is present, call that URL with with_url() to get the next page
  5. Tell folders from files by whether folder or file is set, and write out_children.csv
PythonSign-in required (Microsoft Entra)
"""List a site's document libraries, then every item in one folder of a library."""
import asyncio
import csv
import os
from urllib.parse import quote

from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient

SCOPES = ["Files.Read.All"]
LIBRARY = os.environ.get("LIBRARY_NAME", "Documents")
FOLDER_PATH = os.environ.get("FOLDER_PATH", "Reports")
SELECT = "name,size,lastModifiedDateTime,webUrl,folder,file"


def to_row(item):
    kind = "folder" if item.folder else "file"
    return [kind, item.name, item.size or 0, item.last_modified_date_time, item.web_url]


async def main():
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    drives = await client.sites.by_site_id(os.environ["SITE_ID"]).drives.get()
    drive = next((d for d in drives.value or [] if d.name == LIBRARY), None)
    if drive is None:
        print("library not found:", LIBRARY, [d.name for d in drives.value or []])
        return
    # documented path form; the SDK would encode ":" inside an item id
    url = "https://graph.microsoft.com/v1.0/drives/%s/root:/%s:/children?$select=%s" % (
        drive.id, quote(FOLDER_PATH), SELECT)
    builder = client.drives.by_drive_id(drive.id).items.by_drive_item_id("root").children
    page = await builder.with_url(url).get()
    rows = []
    while page:
        rows += [to_row(i) for i in page.value or []]
        if not page.odata_next_link:
            break
        page = await builder.with_url(page.odata_next_link).get()
    with open("out_children.csv", "w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(["kind", "name", "size", "modified", "webUrl"])
        w.writerows(rows)
    print("items:", len(rows))


if __name__ == "__main__":
    asyncio.run(main())
Output(2026-09-12)
['folder', '2026', 0, None, None]
['file', 'a.docx', 1200, None, None]
Example instruction for Copilot

With msgraph-sdk, write Python that lists the drives of the SITE_ID site, picks the library by name, reads every page of a folder's children (by path, via the root:/{path}:/children URL) with $select, and writes a CSV.

Caution

The API table lists delegated Files.Read as least privileged, but its description is reading the signed-in user's files. For site libraries this tip uses Files.Read.All, described as reading all files the signed-in user can access. Large folders mean many calls, so narrow the scope.

Availability
Microsoft Graph v1.0. Delegated Files.Read.All (documented as not requiring admin consent). Uses msgraph-sdk and azure-identity.
Requires
msgraph-sdk 1.62.0, azure-identity 1.25.3
Permissions
Files.Read.All (delegated)
Tested
run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
Practical edition
Working with OneDrive ── Scope Copilot to a SharePoint library or folder
Source
Microsoft Learn, "List available drives"
Microsoft Learn, "List children of a driveItem"
Microsoft Learn, "Microsoft Graph permissions reference"
Verified
2026-09-12 (v1)
Supporting passages from the sources
To list the document libraries for a site, your app requests the drives relationship on the Site.
GET https://graph.microsoft.com/v1.0/drives/{drive-id}/root:/{path-relative-to-root}:/children
Allows the app to read all files the signed-in user can access.
Allows the app to read the signed-in user's files.

03Download a file's content from a document libraryVerified

GET /drives/{drive-id}/items/{item-id}/content returns the file's primary stream. The response is a 302 redirect to a preauthenticated download URL; content.get() in msgraph-sdk returns bytes, which you write to a new local file.

Approach and steps
  1. Identify the item with DRIVE_ID and ITEM_ID; get item ids from a children or delta listing
  2. First read name, size and file with $select, and stop if file is missing (a folder, for example). Only items with the file property can be downloaded
  3. Write the bytes from content.get() to a new file name with an out_ prefix, never overwriting an existing file
  4. Keep only the file name, dropping any path parts, so nothing is written outside the target folder
  5. For ranged downloads of large files, send the Range header to the @microsoft.graph.downloadUrl URL, not to the /content request
PythonSign-in required (Microsoft Entra)
"""Download one file from a drive to a new local file (never overwrites)."""
import asyncio
import os
from pathlib import Path

from azure.identity import DeviceCodeCredential
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph import GraphServiceClient
from msgraph.generated.drives.item.items.item.drive_item_item_request_builder import (
    DriveItemItemRequestBuilder,
)

SCOPES = ["Files.Read.All"]


def target_path(name, folder="."):
    # keep only the file name and add an out_ prefix; refuse to overwrite
    path = Path(folder) / ("out_" + Path(name).name)
    if path.exists():
        raise FileExistsError(path)
    return path


async def main():
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    item_rb = client.drives.by_drive_id(os.environ["DRIVE_ID"]).items.by_drive_item_id(os.environ["ITEM_ID"])
    query = DriveItemItemRequestBuilder.DriveItemItemRequestBuilderGetQueryParameters(
        select=["name", "size", "file"])
    item = await item_rb.get(request_configuration=RequestConfiguration(query_parameters=query))
    if item.file is None:
        print("not a file:", item.name)
        return
    data = await item_rb.content.get()
    path = target_path(item.name)
    path.write_bytes(data)
    print("saved", path, len(data), "bytes")


if __name__ == "__main__":
    asyncio.run(main())
Output(2026-09-12)
out_報告書.docx
Example instruction for Copilot

With msgraph-sdk, write Python that checks name and file (via $select) for the DRIVE_ID/ITEM_ID item, downloads it with content.get(), and saves it under an out_-prefixed name. Stop on folders and never overwrite an existing file.

Caution

This copies file content to your machine, so follow sensitivity labels and your organization's rules on moving data. The preauthenticated URL returns the content without a token, so keep it out of logs and chats. The local test blocks the network, so following the 302 was not checked against a live response.

Availability
Microsoft Graph v1.0. Delegated Files.Read.All. Uses msgraph-sdk and azure-identity.
Requires
msgraph-sdk 1.62.0, azure-identity 1.25.3
Permissions
Files.Read.All (delegated)
Tested
run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
Source
Microsoft Learn, "Download driveItem content"
Verified
2026-09-12 (v1)
Supporting passages from the sources
Returns a 302 Found response redirecting to a preauthenticated download URL for the file, which is the same URL available through the @microsoft.graph.downloadUrl property on the driveItem.
Only driveItem objects with the file property can be downloaded.
You must append the Range header to the actual @microsoft.graph.downloadUrl URL and not to the request for /content.

04Read list items with column values via expand=fields(select=...)Verified

Adding expand=fields(select=Col1,Col2) to GET /sites/{site-id}/lists/{list-id}/items returns only the named column values in each item's fields. In SharePoint every file is also a list item, so library columns can be read the same way.

Approach and steps
  1. Pass fields(select=FileLeafRef,DocId,Status,ReviewDue,Owner) to expand, using the API-facing column name rather than the display name
  2. Column values land in ListItem.fields.additional_data (a dict), so read them by column name
  3. Results can span several pages, so follow odata_next_link
  4. When narrowing with $filter, use indexed columns; only one indexed field can be filtered at a time
  5. Hidden column values are not returned by default; name those columns in select when you need them
PythonSign-in required (Microsoft Entra)
"""Read list items with selected column values and write them to CSV."""
import asyncio
import csv
import os

from azure.identity import DeviceCodeCredential
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph import GraphServiceClient
from msgraph.generated.sites.item.lists.item.items.items_request_builder import ItemsRequestBuilder

SCOPES = ["Sites.Read.All"]
COLUMNS = ["FileLeafRef", "DocId", "Status", "ReviewDue", "Owner"]


def flatten(item_id, web_url, fields):
    row = {"id": item_id, "webUrl": web_url}
    for c in COLUMNS:
        row[c] = fields.get(c)
    return row


async def main():
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    items_rb = client.sites.by_site_id(os.environ["SITE_ID"]).lists.by_list_id(os.environ["LIST_ID"]).items
    query = ItemsRequestBuilder.ItemsRequestBuilderGetQueryParameters(
        expand=["fields(select=%s)" % ",".join(COLUMNS)])
    page = await items_rb.get(request_configuration=RequestConfiguration(query_parameters=query))
    rows = []
    while page:
        for it in page.value or []:
            fields = it.fields.additional_data if it.fields else {}
            rows.append(flatten(it.id, it.web_url, fields))
        if not page.odata_next_link:
            break
        page = await items_rb.with_url(page.odata_next_link).get()
    with open("out_list_items.csv", "w", encoding="utf-8-sig", newline="") as f:
        w = csv.DictWriter(f, fieldnames=["id", "webUrl"] + COLUMNS)
        w.writeheader()
        w.writerows(rows)
    print("rows:", len(rows))


if __name__ == "__main__":
    asyncio.run(main())
Output(2026-09-12)
15 R-2026-001 None
Example instruction for Copilot

With msgraph-sdk, write Python that reads every page of the SITE_ID/LIST_ID list items with expand=fields(select=FileLeafRef,DocId,Status,ReviewDue,Owner) and writes item id, webUrl and the column values to a CSV. Read values from fields.additional_data.

Caution

If a column name in select is wrong, that column is simply missing from the result; check the API names with the list's columns (separate tip). Sites.Read.All reaches every site you can read, so consider Sites.Selected to confine an app to one site. Decide where exported CSVs with people's names are stored and for how long.

Availability
Microsoft Graph v1.0. Least privileged permission is Sites.Read.All for delegated and application. Personal Microsoft accounts are not supported.
Requires
msgraph-sdk 1.62.0, azure-identity 1.25.3
Permissions
Sites.Read.All (delegated)
Tested
run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
Practical edition
Working with OneDrive ── Scope Copilot to a SharePoint library or folder
Source
Microsoft Learn, "List items"
Microsoft Learn, "columnDefinition resource type"
Microsoft Learn, "Overview of Selected permissions in OneDrive and SharePoint"
Verified
2026-09-12 (v1)
Supporting passages from the sources
GET /sites/{site-id}/lists/{list-id}/items?expand=fields(select=Column1,Column2)
When filtering on indexed fields, the service can only filter one indexed field at a time.
To list hidden field values on listItems, include the desired columns by name in your $select statement.
Within SharePoint, all files are list items, but all list items are not files.

05Fetch only what changed since the last run with a drive delta queryVerified

Following GET /drives/{drive-id}/root/delta to its last page returns an @odata.deltaLink. Save it to a file and call that URL on the next run to get only what was added, changed or deleted since then.

Approach and steps
  1. On the first run call /drives/{drive-id}/root/delta and follow @odata.nextLink while it is returned
  2. Save the @odata.deltaLink from the last page to out_delta_state.json and reuse the URL as is, without parsing it
  3. On the next run call the saved deltaLink with with_url(); remove items that carry the deleted facet from your local record
  4. If the service answers 410 Gone, drop the saved state and start again with a full enumeration
  5. To track changes from now on without reading everything, request token=latest to get just a deltaLink
PythonSign-in required (Microsoft Entra)
"""Track changes in a drive: save the deltaLink and resume from it on the next run."""
import asyncio
import json
import os
from pathlib import Path

from azure.identity import DeviceCodeCredential
from kiota_abstractions.api_error import APIError
from msgraph import GraphServiceClient

SCOPES = ["Files.Read.All"]
STATE = Path(os.environ.get("DELTA_STATE", "out_delta_state.json"))


def first_link(drive_id):
    return "https://graph.microsoft.com/v1.0/drives/%s/root/delta" % drive_id


def load_link(drive_id):
    if STATE.exists():
        return json.loads(STATE.read_text(encoding="utf-8"))["deltaLink"]
    return first_link(drive_id)


def summarize(items):
    changed = [i["name"] for i in items if not i["deleted"]]
    removed = [i["id"] for i in items if i["deleted"]]
    return changed, removed


async def read_all(builder, link):
    items, page = [], await builder.with_url(link).get()
    while True:
        items += [{"id": i.id, "name": i.name, "deleted": i.deleted is not None} for i in page.value or []]
        if not page.odata_next_link:
            return items, page.odata_delta_link
        page = await builder.with_url(page.odata_next_link).get()


async def main():
    drive_id = os.environ["DRIVE_ID"]
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    builder = client.drives.by_drive_id(drive_id).items.by_drive_item_id("root").delta
    try:
        items, delta_link = await read_all(builder, load_link(drive_id))
    except APIError as e:
        if e.response_status_code != 410:
            raise
        # the saved token can no longer be used: enumerate everything again
        items, delta_link = await read_all(builder, first_link(drive_id))
    changed, removed = summarize(items)
    print("changed:", len(changed), "deleted:", len(removed))
    for name in changed[:10]:
        print(" ", name)
    STATE.write_text(json.dumps({"deltaLink": delta_link}), encoding="utf-8")


if __name__ == "__main__":
    asyncio.run(main())
Output(2026-09-12)
(['a.docx'], ['2'])
delta_link_attr True
Example instruction for Copilot

With msgraph-sdk, write Python that runs a delta query on the DRIVE_ID drive, follows nextLink to the end, saves the deltaLink to a JSON file, and resumes from it next time. On 410, restart with a full enumeration, and print how many items were deleted.

Caution

Deltas include deletions, so remember to remove them locally. Do not save the deltaLink when a run fails midway, so the next run reads the same range again. After a 410 you must re-read everything and reconcile it with your local record.

Availability
Microsoft Graph v1.0. Delegated Files.Read.All (the API table lists Files.Read as least privileged). OneDrive and SharePoint support token=latest.
Requires
msgraph-sdk 1.62.0, azure-identity 1.25.3
Permissions
Files.Read.All (delegated)
Tested
run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
Source
Microsoft Learn, "driveItem: delta"
Microsoft Learn, "Use delta query to track changes in Microsoft Graph data"
Verified
2026-09-12 (v1)
Supporting passages from the sources
To check for changes in the future, call delta again with the @odata.deltaLink from the previous response.
In these cases the service returns an HTTP 410 Gone error with an error response containing one of the error codes below, and a Location header containing a new nextLink that starts a fresh delta enumeration from scratch.
you can copy and apply the @odata.nextLink or @odata.deltaLink URL to the next delta function call without having to inspect the contents of the URL, including its state token.
To retrieve the latest deltaLink, call delta with a query string parameter ?token=latest.

06Confine an app to assigned sites with Sites.SelectedVerified

Consenting to Sites.Selected alone grants no access. Access to a site starts only after a role such as read is assigned to the app with POST /sites/{siteId}/permissions. It works in both application and delegated modes.

Approach and steps
  1. Add Sites.Selected (application or delegated) to the app in Entra ID and consent. At this point the app can read no site
  2. On the site side, call POST /sites/{siteId}/permissions with roles (read, write, owner or fullcontrol) and the app in grantedToIdentities. Making this assignment requires Sites.FullControl.All
  3. For app-only runs, get the token with a certificate (CertificateCredential) locally or a managed identity on Azure, using the .default scope
  4. In delegated mode the app can use only what both the app and the signed-in user are allowed
  5. To stop access, delete the site assignment or revoke the consent in Entra ID; either one blocks the app
PythonSign-in required (Microsoft Entra)
"""App-only read of a site that was assigned to this app through Sites.Selected."""
import asyncio
import os

from azure.identity import CertificateCredential, ManagedIdentityCredential
from msgraph import GraphServiceClient

# app-only tokens use .default; the app registration holds Sites.Selected
SCOPES = ["https://graph.microsoft.com/.default"]


def make_credential():
    cert = os.environ.get("CERT_PATH", "")
    if cert and os.path.exists(cert):
        return CertificateCredential(
            tenant_id=os.environ["TENANT_ID"], client_id=os.environ["CLIENT_ID"], certificate_path=cert)
    # on Azure (Functions, VM) use a managed identity instead of a certificate file
    return ManagedIdentityCredential(client_id=os.environ.get("AZURE_CLIENT_ID"))


def describe(site_name, lists):
    lines = ["site: %s" % site_name]
    lines += ["  list: %s  %s" % (name, url) for name, url in lists]
    return "\n".join(lines)


async def main():
    client = GraphServiceClient(credentials=make_credential(), scopes=SCOPES)
    site_rb = client.sites.by_site_id(os.environ["SITE_ID"])
    site = await site_rb.get()
    lists = await site_rb.lists.get()
    print(describe(site.display_name, [(x.display_name, x.web_url) for x in lists.value or []]))


if __name__ == "__main__":
    asyncio.run(main())
Output(2026-09-12)
site: docs
  list: 文書ライブラリ  https://example.sharepoint.com/sites/docs/Shared%20Documents
ManagedIdentityCredential
Example instruction for Copilot

Write msgraph-sdk Python for an app granted through Sites.Selected that prints the SITE_ID site name and its lists. Use CertificateCredential when the CERT_PATH certificate exists, otherwise ManagedIdentityCredential, with the https://graph.microsoft.com/.default scope.

Caution

The docs call app-only tokens higher risk because no user is present, and prefer delegated when possible. Sites.FullControl.All, needed for the assignment, is powerful, so an administrator makes the assignment and the reading app never holds it. Assigning to lists or files breaks inheritance, so watch the limits on unique permissions.

Availability
Microsoft Graph v1.0. Sites.Selected exists for application and delegated (application requires admin consent). The site assignments are configured in SharePoint Online.
Requires
msgraph-sdk 1.62.0, azure-identity 1.25.3
Permissions
Sites.Selected (application)
Tested
run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
Practical edition
Grounding Copilot in Your Stored Content ── Copilot surfaces only data you can at least view
Source
Microsoft Learn, "Overview of Selected permissions in OneDrive and SharePoint"
Microsoft Learn, "Microsoft Graph permissions reference"
Verified
2026-09-12 (v1)
Supporting passages from the sources
Selected scopes require an explicit assignment action; an application consented for Lists.SelectedOperations.Selected would initially have no access.
Now, lists, list items, folders, and files are also supported, and all Selected scopes now support delegated and application modes.
In the delegated scenario, both the application and user permissions are calculated and then intersected
Because you can grant full control permissions to a site collection by using Sites.Selected, this requirement is necessarily high.
Application only scenarios have no user present and are considered higher risk.
The specific site collections and the permissions granted will be configured in SharePoint Online.

07Find anyone and organization-wide sharing links on files in a folderVerified

In GET /drives/{drive-id}/items/{item-id}/permissions, entries with a link are sharing links: scope anonymous means anyone can use it, organization means anyone signed in to the same tenant. Walk a folder's files and write the broad links to a CSV.

Approach and steps
  1. Collect file ids and names from the folder's children; permissions cannot be expanded in a listing, so call it per file
  2. Get each file's permissions and keep only entries that have a link
  3. Treat scope anonymous or organization as broad, and record link.type, roles and expirationDateTime with it
  4. Run as the file owner; a non-owner only gets the sharing permissions that apply to them
  5. Review the CSV with the owners and remove unneeded links through a separate process (this code removes nothing)
PythonSign-in required (Microsoft Entra)
"""List anyone/organization sharing links on the files of one folder (read only)."""
import asyncio
import csv
import os
from urllib.parse import quote

from azure.identity import DeviceCodeCredential
from msgraph import GraphServiceClient

SCOPES = ["Files.Read.All"]
FOLDER_PATH = os.environ.get("FOLDER_PATH", "Reports")
BROAD = {"anonymous", "organization"}


def broad_links(file_name, perms):
    out = []
    for p in perms:
        link = p.get("link") or {}
        if link.get("scope") in BROAD:
            out.append([file_name, link["scope"], link.get("type"), ",".join(p.get("roles") or []),
                        p.get("expirationDateTime") or ""])
    return out


def as_dict(p):
    link = {"scope": p.link.scope, "type": p.link.type} if p.link else None
    exp = p.expiration_date_time.isoformat() if p.expiration_date_time else ""
    return {"roles": p.roles, "link": link, "expirationDateTime": exp}


async def main():
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    drive_rb = client.drives.by_drive_id(os.environ["DRIVE_ID"])
    url = "https://graph.microsoft.com/v1.0/drives/%s/root:/%s:/children?$select=id,name,file" % (
        os.environ["DRIVE_ID"], quote(FOLDER_PATH))
    children_rb = drive_rb.items.by_drive_item_id("root").children
    page, files = await children_rb.with_url(url).get(), []
    while page:
        files += [i for i in page.value or [] if i.file]
        if not page.odata_next_link:
            break
        page = await children_rb.with_url(page.odata_next_link).get()
    rows = []
    for f in files:
        perms = await drive_rb.items.by_drive_item_id(f.id).permissions.get()
        rows += broad_links(f.name, [as_dict(p) for p in perms.value or []])
    with open("out_broad_links.csv", "w", encoding="utf-8-sig", newline="") as fh:
        w = csv.writer(fh)
        w.writerow(["file", "scope", "type", "roles", "expires"])
        w.writerows(rows)
    print("files:", len(files), "broad links:", len(rows))


if __name__ == "__main__":
    asyncio.run(main())
Output(2026-09-12)
broad: 2
['提案書.docx', 'anonymous', 'view', 'read', '']
['提案書.docx', 'organization', 'edit', 'write', '2026-12-31']
Example instruction for Copilot

With msgraph-sdk, write Python that fetches permissions for each file under FOLDER_PATH in the DRIVE_ID drive and writes only sharing links whose link.scope is anonymous or organization to a CSV (file, scope, type, roles, expiration). Put the check in its own function.

Caution

Non-owner callers see only sharing that applies to them, so an empty result does not prove a file is safe. OneDrive for Business and SharePoint libraries do not return inheritedFrom, so this output cannot tell you whether a permission came from a parent. Use the admin reports for tenant-wide oversharing.

Availability
Microsoft Graph v1.0. Delegated Files.Read.All (the API table lists Files.Read as least privileged).
Requires
msgraph-sdk 1.62.0, azure-identity 1.25.3
Permissions
Files.Read.All (delegated)
Tested
run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
Practical edition
Working with OneDrive ── Your sharing settings define what Copilot can surface
Grounding Copilot in Your Stored Content ── Find oversharing with data access governance reports
Source
Microsoft Learn, "sharingLink resource type"
Microsoft Learn, "List sharing permissions on a driveItem"
Microsoft Learn, "permission resource type"
Verified
2026-09-12 (v1)
Supporting passages from the sources
Value anonymous indicates the link is usable by anyone, organization indicates the link is only usable for users signed into the same tenant.
For a non-owner caller, only the sharing permissions that apply to the caller are returned.
The permissions relationship of DriveItem cannot be expanded as part of a call to get DriveItem or a collection of DriveItems.
OneDrive for Business and SharePoint document libraries don't return the inheritedFrom property.

08Audit library columns for empty required values and past-due datesVerified

Read GET .../lists/{list-id}/columns to find columns with required set to true, then compare each item's fields to catch empty values. A date column earlier than the reference date counts as past due. The checks live in audit(), tested with sample JSON.

Approach and steps
  1. Read the columns and collect the name of each column where required is true and hidden and readOnly are false; name is the key used in fields
  2. Read items with expand=fields(select=...) for just the required and date columns
  3. audit() separates items with a missing or empty required value from items whose ReviewDue is before the reference date
  4. Pass the reference date as an argument, and use a fixed date in tests so results do not change day to day
  5. Write out_column_audit.csv and ask the owners to check it; the code does not change any values
PythonSign-in required (Microsoft Entra)
"""Audit a library: empty required columns and past-due dates (report only)."""
import asyncio
import csv
import datetime
import os

from azure.identity import DeviceCodeCredential
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph import GraphServiceClient
from msgraph.generated.sites.item.lists.item.items.items_request_builder import ItemsRequestBuilder

SCOPES = ["Sites.Read.All"]
DATE_COL = os.environ.get("DATE_COLUMN", "ReviewDue")


def audit(items, required, date_col, today):
    rows = []
    for it in items:
        f = it.get("fields") or {}
        name = f.get("FileLeafRef", it.get("id"))
        for col in required:
            if f.get(col) in (None, ""):
                rows.append([it.get("id"), name, "missing:" + col, it.get("webUrl")])
        due = f.get(date_col)
        if due and str(due)[:10] < today:
            rows.append([it.get("id"), name, "overdue", it.get("webUrl")])
    return rows


async def fetch(client, site_id, list_id):
    list_rb = client.sites.by_site_id(site_id).lists.by_list_id(list_id)
    cols = await list_rb.columns.get()
    required = [c.name for c in cols.value or [] if c.required and not c.hidden and not c.read_only]
    select = ",".join(["FileLeafRef", DATE_COL] + required)
    query = ItemsRequestBuilder.ItemsRequestBuilderGetQueryParameters(expand=["fields(select=%s)" % select])
    page = await list_rb.items.get(request_configuration=RequestConfiguration(query_parameters=query))
    items = []
    while page:
        items += [{"id": i.id, "webUrl": i.web_url, "fields": i.fields.additional_data if i.fields else {}}
                  for i in page.value or []]
        if not page.odata_next_link:
            break
        page = await list_rb.items.with_url(page.odata_next_link).get()
    return items, required


async def main():
    cred = DeviceCodeCredential(client_id=os.environ["CLIENT_ID"], tenant_id=os.environ["TENANT_ID"])
    client = GraphServiceClient(credentials=cred, scopes=SCOPES)
    items, required = await fetch(client, os.environ["SITE_ID"], os.environ["LIST_ID"])
    rows = audit(items, required, DATE_COL, datetime.date.today().isoformat())
    with open("out_column_audit.csv", "w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(["id", "file", "problem", "webUrl"])
        w.writerows(rows)
    print("required:", required, "problems:", len(rows))


if __name__ == "__main__":
    asyncio.run(main())
Output(2026-09-12)
missing: 14 overdue: 2
['1', '文書01.pdf', 'missing:Owner', 'https://example.sharepoint.com/sites/docs/Shared%20Documents/01.pdf']
['2', '文書02.pdf', 'missing:Status', 'https://example.sharepoint.com/sites/docs/Shared%20Documents/02.pdf']
['2', '文書02.pdf', 'overdue', 'https://example.sharepoint.com/sites/docs/Shared%20Documents/02.pdf']
['5', '文書05.pdf', 'missing:DocId', 'https://example.sharepoint.com/sites/docs/Shared%20Documents/05.pdf']
Example instruction for Copilot

Write a Python audit(items, required, date_col, today) that takes Graph listItem JSON and reports items with empty required columns and items whose ReviewDue is before the reference date. Get required columns from the list's columns, and keep the msgraph-sdk calls in a separate function.

Caution

required describes the column; it does not mean existing items actually hold a value. Dates are compared as ISO strings, so align the comparison for columns that include time or time zone. If owner names are exported, decide where the file is kept and who sees it. Have a person review the result before notifying anyone.

Availability
Microsoft Graph v1.0. Sites.Read.All is the least privileged permission for reading both columns and items.
Requires
msgraph-sdk 1.62.0, azure-identity 1.25.3
Permissions
Sites.Read.All (delegated)
Tested
run up to the sign-in step (not connected to Microsoft 365); aggregation/checking logic run on sample data (2026-09-12, Python 3.12.10 (venv) / Windows 11, msgraph-sdk 1.62.0, azure-identity 1.25.3)
Practical edition
Working with OneDrive ── Scope Copilot to a SharePoint library or folder
Source
Microsoft Learn, "columnDefinition resource type"
Microsoft Learn, "List columnDefinitions in a list"
Microsoft Learn, "List items"
Verified
2026-09-12 (v1)
Supporting passages from the sources
Specifies whether the column value isn't optional.
The API-facing name of the column as it appears in the fields on a listItem.
GET /sites/{site-id}/lists/{list-id}/columns