Run an Active Directory script
executeRun a JavaScript script against an Active Directory domain to query, filter, join, count, or change objects; AD permissions govern results.
Instructions
Run a JavaScript script against one Active Directory domain. Use this for any read, filter, join, count, or change. Write the body of an async function and "return" the value you want back. Only what you return (and console.log) comes back to you, so filter and pick attributes inside the script. Output is capped at about 10,000 tokens.
Every call runs as the Windows user the server runs as, over Kerberos with signing and sealing, against the domain's PDC emulator, so Active Directory's own permissions decide what succeeds. Use connections_list to find domain aliases. Name objects by DN. Pass your own attributes; without them you get name, objectClass and sAMAccountName. A connection added in read mode refuses add, modify, delete, move, addAce and removeAce.
Available in the script: declare const ad: { // One object by DN, or null if there is none. Default attributes: name, objectClass, sAMAccountName. ["*"] for all. get(dn: string, attributes?: string[], opts?: { controls?: Controls }): Promise<Entry | null>; // Objects matching an LDAP filter. base defaults to the domain head, scope to "sub", max to 1000 (cap 20000). // more is true when there were more than max. Multi-valued attributes like member come back whole. search(opts: { filter?: string; base?: string; scope?: "base" | "one" | "sub"; attributes?: string[]; max?: number; controls?: Controls }): Promise<{ entries: Entry[]; more: boolean }>; // The account the server runs as, and the PDC emulator it talks to. whoami(): Promise<{ user: string; pdc: string; defaultNamingContext: string }>; // Writes. A read-mode connection refuses these four before anything is sent. add(dn: string, attributes: Record<string, Value | Value[]>): Promise<{ dn: string }>; // include objectClass modify(dn: string, changes: Array<{ op: "add" | "replace" | "delete"; attribute: string; values?: Value | Value[] }>, opts?: { controls?: Controls }): Promise<{ dn: string }>; delete(dn: string, opts?: { tree?: boolean }): Promise<{ dn: string }>; move(dn: string, to: { newParent?: string; newName?: string }): Promise<{ dn: string }>; // newName is an RDN, e.g. "CN=New Name" // Permissions. getAcl reads the owner and DACL, which needs no admin rights. A read-mode connection refuses addAce and removeAce. getAcl(dn: string): Promise<{ dn: string; owner: Principal; protected: boolean; aces: Ace[] }>; // objectType: an attribute, class or extended right name (find it with search) or a GUID. inheritedObjectType: a class. addAce(dn: string, ace: NewAce): Promise<{ dn: string }>; // Removes the entry that matches exactly; an Ace from getAcl can be passed back as it is. removed is false if none matched. removeAce(dn: string, ace: NewAce | Ace): Promise<{ dn: string; removed: boolean }>; }; // Group Policy, through Microsoft's GroupPolicy module on the PDC emulator. g is a GPO's name or id. // A read-mode connection refuses create, delete, link, unlink, set and remove. declare const gpo: { list(): Promise<Gpo[]>; // With where it is linked and every registry policy value it sets. get(g: string): Promise<Gpo & { links: Link[]; computerSettings: Setting[]; userSettings: Setting[]; securitySettings: SecuritySettings }>; create(name: string, opts?: { comment?: string }): Promise; delete(g: string): Promise<{ id: string; name: string; deleted: true }>; // Creates the link, or changes it if the GPO is already linked there. target is an OU or the domain DN. link(g: string, target: string, opts?: { enabled?: boolean; enforced?: boolean; order?: number }): Promise<{ id: string; links: Link[] }>; unlink(g: string, target: string): Promise<{ id: string; links: Link[] }>; // key starts with HKLM\ (computer) or HKCU\ (user). Find keys with search over catalogue.policies. set(g: string, s: { key: string; valueName: string; type: "String" | "ExpandString" | "DWord" | "QWord" | "MultiString"; value: string | number | string[] }): Promise; remove(g: string, key: string, valueName?: string): Promise; // Backup-GPO to a folder on the machine adslayer runs on. Allowed on a read connection. backup(g: string, path: string): Promise<{ id: string; backupId: string; path: string; timestamp: string }>; }; interface Gpo { id: string; name: string; status: string; created: string; modified: string; computerVersion: number; userVersion: number; wmiFilter: string | null } interface Link { target: string; enabled: boolean; enforced: boolean; order: number } interface Setting { key: string; valueName: string; type: string; value: unknown } // From the GPO's security template (GptTmpl.inf). Read-only for now. Empty when the GPO sets none. interface SecuritySettings { systemAccess: Record<string, number | string>; // password and lockout policy, e.g. MinimumPasswordLength, LockoutBadCount eventAudit: Record<string, number | string>; privilegeRights: Record<string, GpoPrincipal[]>; // user rights, e.g. SeInteractiveLogonRight groupMembership: Array<{ group: GpoPrincipal; members?: GpoPrincipal[]; memberOf?: GpoPrincipal[] }>; // Restricted Groups registryValues: Record<string, { type: number; value: number | string }>; // Security Options, keyed MACHINE... other: Record<string, Record<string, string>>; } interface GpoPrincipal { sid: string | null; name: string | null } // Every attribute is an array. GUIDs and SIDs are strings; other binary values are { base64 }. interface Entry { dn: string; attributes: Record<string, Array<string | { base64: string }>> } type Value = string | number | boolean | { base64: string }; interface Principal { sid: string; name: string | null } // name e.g. "CONTOSO\Helpdesk" // rights: e.g. "GenericAll", "ReadProperty", "WriteProperty", "ExtendedRight", "CreateChild", "DeleteChild", "Delete", "DeleteTree", "WriteDacl" // inheritance: "All" = this object and everything below it; "Descendents" = only below it. type Inheritance = "None" | "All" | "Descendents" | "SelfAndChildren" | "Children"; interface Ace { principal: Principal; type: "allow" | "deny"; rights: string[]; objectType?: string; inheritedObjectType?: string; inheritance: Inheritance; inherited: boolean } interface NewAce { principal: string; type: "allow" | "deny"; rights: string[]; objectType?: string; inheritedObjectType?: string; inheritance?: Inheritance } // principal: "CONTOSO\Helpdesk" or a SID // showDeleted: see and restore objects in the Recycle Bin (CN=Deleted Objects). Restore = modify with it. interface Controls { showDeleted?: true } // Calls may run in parallel with Promise.all. Each run may make at most 200 calls.
Examples: // Enabled users in an OU, by name const r = await ad.search({ base: "OU=Sales,DC=contoso,DC=local", filter: "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))", attributes: ["sAMAccountName"] }); return r.entries.map(e => e.attributes.sAMAccountName[0]);
// How many members a group has, nested groups not expanded const g = await ad.get("CN=Helpdesk,OU=Groups,DC=contoso,DC=local", ["member"]); return g?.attributes.member?.length ?? 0;
// Disable a user (514 = normal account + disabled) return await ad.modify("CN=Jane Doe,OU=Sales,DC=contoso,DC=local", [{ op: "replace", attribute: "userAccountControl", values: "514" }]);
// Reset a password. unicodePwd takes the password in double quotes as UTF-16LE bytes. Add pwdLastSet "0" to force a change at next sign-in. const pw = '"' + newPassword + '"'; let b = ""; for (let i = 0; i < pw.length; i++) { const c = pw.charCodeAt(i); b += String.fromCharCode(c & 255, c >> 8); } return await ad.modify("CN=Jane Doe,OU=Sales,DC=contoso,DC=local", [{ op: "replace", attribute: "unicodePwd", values: { base64: btoa(b) } }, { op: "replace", attribute: "pwdLastSet", values: "0" }]);
// Restore a deleted user from the Recycle Bin to where it was. Delete isDeleted first, then set the new DN. const controls = { showDeleted: true }; const gone = (await ad.search({ base: "CN=Deleted Objects,DC=contoso,DC=local", filter: "(&(isDeleted=TRUE)(sAMAccountName=jdoe))", attributes: ["lastKnownParent", "msDS-LastKnownRDN"], controls })).entries[0]; const to = "CN=" + gone.attributes["msDS-LastKnownRDN"][0] + "," + gone.attributes.lastKnownParent[0]; return await ad.modify(gone.dn, [{ op: "delete", attribute: "isDeleted" }, { op: "replace", attribute: "distinguishedName", values: to }], { controls });
// Who can reset passwords in an OU: full control, all extended rights, or the Reset Password right const acl = await ad.getAcl("OU=Sales,DC=contoso,DC=local"); return acl.aces.filter(a => a.type === "allow" && (a.rights.includes("GenericAll") || (a.rights.includes("ExtendedRight") && (!a.objectType || a.objectType === "User-Force-Change-Password")))).map(a => a.principal.name ?? a.principal.sid);
// Password and lockout policy for the domain, from the Default Domain Policy's security settings const sa = (await gpo.get("Default Domain Policy")).securitySettings.systemAccess; return { minLength: sa.MinimumPasswordLength, lockoutAfter: sa.LockoutBadCount };
// Protect an OU from accidental deletion, as the admin tools do. AD allows a delete with Delete on the object or // DeleteChild on its parent, so deny both. To undo, removeAce the first; the parent's deny also protects its other children. await ad.addAce("OU=Sales,DC=contoso,DC=local", { principal: "S-1-1-0", type: "deny", rights: ["Delete", "DeleteTree"] }); return await ad.addAce("DC=contoso,DC=local", { principal: "S-1-1-0", type: "deny", rights: ["DeleteChild"] });
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | Body of an async JavaScript function. Use await ad.* and return a value. | |
| domain | Yes | Connection alias or the domain's DNS name. See connections_list. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| ok | Yes | ||
| logs | Yes | ||
| calls | Yes | ||
| error | No | ||
| domain | Yes | ||
| result | No | ||
| truncated | Yes |