Setting StrongAuthenticationUserDetails PhoneNumber for AzureAD via Powershell? - powershell

That title really flows.
When setting up computers for use with Azure Active Directory, we would have IT do initial setup and config. This included the first sign in and joining to Azure Active Directory. When signing in it forces you to select a verification method. We would use our desk phone or cell phone for ease.
The time has come for us to update that second factor phone number. I know of a way to manually do it via the Azure AD Web UI, but I am looking for a scripted way to set that number in PowerShell.
Here is how I retrieve the number via PowerShell.
Get-msoluser -UserPrincipalName "email#emailaddress.com" | Select-Object -ExpandProperty StrongAuthenticationUserDetails
That code returns this info:
ExtensionData : System.Runtime.Serialization.ExtensionDataObject
AlternativePhoneNumber :
Email :
OldPin :
PhoneNumber : +1 5554445555
Pin :
However, there seems to be no similar option for setting the StrongAuthenticationUserDetails.
All my searches just turned up how to bulk enable 2-factor authentication, which is not what I want to do. I want to leave the StrongAuthentication the same while only updating the phone number.

As I said in comment, it appears there is read-only access for powershell.
There is even opened ticket for that on Azure feedback.
There is a plan to do it, but no ETA. My guess is that you will have to wait if you want to use powershell only.
As workaround, you could use powershell & watir for .NET OR Watin with Watin recorder to automatize it via Internet Explorer. As I don't have a testing Azure; I can not create workable code for you.
Using Watin and powershell - you could check: https://cmille19.wordpress.com/2009/09/01/internet-explorer-automation-with-watin/
The following text and code, I wanted to backup it here, was taken from the above page (all credits to the author):
Next click the record button and click the HTML element you want to
automate. Then stop the WatIN recorder and click copy code to
clipboard icon. This will produce some C# code that just needs to be
translated into PowerShell:
// Windows
WatiN.Core.IE window = new WatiN.Core.IE();
// Frames
Frame frame_sd_scoreboard = window.Frame(Find.ByName("sd") && Find.ByName("scoreboard"));
// Model
Element __imgBtn0_button = frame_sd_scoreboard.Element(Find.ByName("imgBtn0_button"));
// Code
__imgBtn0_button.Click();
window.Dispose();
So, I now know the name of the button and that it is 3 frames deep. A
little WatIN object exploration later, I came up with the follow
script, which clicks a button every 50 mintues.
#Requires -version 2.0
#powershell.exe -STA
[Reflection.Assembly]::LoadFrom( "$ProfileDirLibrariesWatiN.Core.dll" ) | out-null
$ie = new-object WatiN.Core.IE("https://sd.acme.com/CAisd/pdmweb.exe")
$scoreboard = $ie.frames | foreach {$_.frames } | where {$_.name –eq ‘sd’} | foreach {$_.frames } | where {$_.name –eq ‘scoreboard’}
$button = $scoreboard.Element("imgBtn0_button")
while ($true)
{
$button.Click()
#Sleep for 50 minutes
[System.Threading.Thread]::Sleep(3000000)
}

Disclaimer: the code is provided as-is. It might happen that it'll stop working in case MS changes Azure Portal interface.
I'm using the following Greasemonkey script to update alternate email and phone (phone can be updated via Graph API now so the script will be useful for email only):
// ==UserScript==
// #name Unnamed Script 548177
// #version 1
// #grant none
// #namespace https://portal.azure.com
// ==/UserScript==
(function(){
document.addEventListener('keydown', function(e) {
// press alt+shift+g
if (e.keyCode == 71 && e.shiftKey && !e.ctrlKey && e.altKey && !e.metaKey) {
const url = document.URL;
const regex = /https:\/\/portal.azure.com\/#blade\/Microsoft_AAD_IAM\/UserDetailsMenuBlade\/UserAuthMethods\/userId\/[\w-]+\/adminUnitObjectId[\/]*\?\w+=(\d{9})&\w+=([\w\.-#]+)/;
const params = url.match(regex);
const allAuthRows = document.getElementsByClassName('ext-userauthenticationmethods-section-row');
const authRowsArray = Array.from(allAuthRows);
let emailRow;
let phoneRow;
let i;
for (i =0; i < authRowsArray.length; i++) {
if (authRowsArray[i].childNodes[1].childNodes[1].childNodes[0].data === 'Email') {
emailRow = authRowsArray[i]
}
if (authRowsArray[i].childNodes[1].childNodes[1].childNodes.length > 1) {
if (authRowsArray[i].childNodes[1].childNodes[1].childNodes[1].childNodes[0].data === 'Phone') {
phoneRow = authRowsArray[i]
}
}
}
const emailInput = emailRow.childNodes[3].childNodes[1].childNodes[1].childNodes[0].childNodes[0].childNodes[0].childNodes[2];
const phoneInput = phoneRow.childNodes[3].childNodes[1].childNodes[1].childNodes[0].childNodes[0].childNodes[0].childNodes[2];
const event = new Event('input', {
'bubbles': true,
'cancelable': true
});
if (params[1] !== '000000000') {
phoneInput.value = `+48 ${params[1]}`;
phoneInput.dispatchEvent(event);
}
if (params[2] !== 'null') {
emailInput.value = params[2];
emailInput.dispatchEvent(event);
}
setTimeout(() => {
const buttonArr = document.getElementsByClassName('azc-toolbarButton-container fxs-portal-hover');
const saveButton = Array.from(buttonArr).find(e => e.title === 'Save');
saveButton.click();
} , 2000);
}
}, false);
})();
It requires you to open Azure portal with querystring like this (I do it with PowerShell):
https://portal.azure.com/#blade/Microsoft_AAD_IAM/UserDetailsMenuBlade/UserAuthMethods/userId/$($u.ObjectId)/adminUnitObjectId/?param1=$newPhone&param2=$newMail
How to use it:
open only one tab at a time, otherwise you'll receive Unable to sign-in error
from time to time you'll receive that error anyway, so just wait
to trigger the script press Alt+Shift+g after the site is loaded (you can change the shortcut in first if)
once the data is updated and saved, press Ctrl+w to close the current tab and press Alt+Tab to switch to previous window (should be PowerShell)
you're still free to use the code to update the phone. Make sure to change country code (currently +48 for Poland)

Related

How can I replicate New-SmbGlobalMapping in C# code?

I am writing a service which controls docker containers. I want to have the mounted volume as an Azure share, and thus need to use the SMB Global Mapping. If I use the usual WNetAddConnection2A then I can mount the share just fine in my code, but the containers cannot see it as it is not "global". I can't find source for the PowerShell New-SmbGlobalMapping command (is there a way to see it?) and I can't find a suitable API to call. I hope someone knows the magic incantation I can put in my .NET code.
I can't find source for the PowerShell New-SmbGlobalMapping command
(is there a way to see it?) and I can't find a suitable API to call. I
hope someone knows the magic incantation I can put in my .NET code.
PowerShell uses WMI
In your case, it calls
Create method of the MSFT_SmbMapping class (MSFT_SmbGlobalMapping exactly)
You can use WMI Code Creator to generate/test C# code
EDIT : Test with PowerShell.Create
Test as Admin ("requireAdministrator" in manifest) on Windows 10
Test code (C#, VS 2015) =>
// PowerShell calls CredUIPromptForCredentialsW to display the User/Password dialog (you can call it with P/Invoke if needed)
string sUser = "user#provider.com";
string sPassword = "myPassword";
System.Net.NetworkCredential networkCredential = new System.Net.NetworkCredential(sUser, sPassword, null);
System.Security.SecureString securePassword = new System.Security.SecureString();
foreach (var c in networkCredential.Password)
securePassword.AppendChar(c);
// Add reference to :
// C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll
// Add :
// using System.Management.Automation;
PSCredential psCredential = new PSCredential(networkCredential.UserName, securePassword);
// Error handling must be improved : if I pass an invalid syntax for "RemotePath" or not launched as Admin,
// nothing happens (no error, no result) (on Windows 10)
string sLocalPath = "Q:";
string sRemotePath = "\\\\DESKTOP-EOPIFM5\\Windows 7";
using (var ps = PowerShell.Create())
{
ps.AddCommand("New-SmbGlobalMapping");
ps.AddParameter("LocalPath", sLocalPath);
ps.AddParameter("RemotePath", sRemotePath);
ps.AddParameter("Credential", psCredential);
//ps.AddParameter("RequireIntegrity", false);
//ps.AddParameter("RequirePrivacy", false);
try
{
System.Collections.ObjectModel.Collection<PSObject> collectionResults = ps.Invoke();
foreach (PSObject psObl in collectionResults)
{
Console.WriteLine("Status : {0}", psObl.Members["Status"].Value.ToString());
Console.WriteLine("Local Path : {0}", psObl.Members["LocalPath"].Value.ToString());
Console.WriteLine("Remote Path : {0}\n", psObl.Members["RemotePath"].Value.ToString());
}
}
catch (ParameterBindingException pbe)
{
System.Console.WriteLine("\rNew-SmbGlobalMapping error : {0}: {1}",
pbe.GetType().FullName, pbe.Message);
}
}
// To get and remove the test mapping in PowerShell :
// Get-SmbGlobalMapping
// Remove-SmbGlobalMapping -RemotePath "\\DESKTOP-EOPIFM5\Windows 7" -Force

How can I can list of alerts associated with scan rules in OWASP ZAP?

I want to get the list of alerts in a tabular form like below. I copy the URL's in the alerts and manually prepare such a tabular table myself. However, I need to do this automatically or semi-automatically (at least)
Alert Name URL Scan Type Scan_Name WASCID CWEID
---------- --------------- --------- --------- ----- ------
You can export the report in XML and apply any kind of XSL transform to it that you might like.
You could pull the XML report into Excel (or whatever spreadsheet program) and manipulate it.
You could pull alerts from the web API and have them in XML or json and process them however you like programmatically.
You could write a standalone script (within ZAP) to traverse the Alerts tree and output the details tab delimited in the script console pane. For example:
extAlert = org.parosproxy.paros.control.Control.getSingleton().
getExtensionLoader().getExtension(
org.zaproxy.zap.extension.alert.ExtensionAlert.NAME)
extPscan = org.parosproxy.paros.control.Control.getSingleton().
getExtensionLoader().getExtension(
org.zaproxy.zap.extension.pscan.ExtensionPassiveScan.NAME);
var pf = Java.type("org.parosproxy.paros.core.scanner.PluginFactory");
printHeaders();
if (extAlert != null) {
var Alert = org.parosproxy.paros.core.scanner.Alert;
var alerts = extAlert.getAllAlerts();
for (var i = 0; i < alerts.length; i++) {
var alert = alerts[i]
printAlert(alert);
}
}
function printHeaders() {
print('AlertName\tSource:PluginName\tWASC\tCWE');
}
function printAlert(alert) {
var scanner = '';
// If the session is loaded in ZAP and one of the extensions that provided a plugin for the
// existing alerts is missing (ex. uninstalled) then plugin (below) will be null, and hence scanner will end-up being empty
if (alert.getSource() == Alert.Source.ACTIVE) {
plugin = pf.getLoadedPlugin(alert.getPluginId());
if (plugin != null) {
scanner = plugin.getName();
}
}
if (alert.getSource() == Alert.Source.PASSIVE && extPscan != null) {
plugin = extPscan.getPluginPassiveScanner(alert.getPluginId());
if (plugin != null) {
scanner = plugin.getName();
}
}
print(alert.getName() + '\t' + alert.getSource() + ':' + scanner + '\t' + alert.getWascId() + '\t' + alert.getCweId());
// For more alert properties see https://static.javadoc.io/org.zaproxy/zap/2.7.0/org/parosproxy/paros/core/scanner/Alert.html
}
Produces script console output like (note the 2nd, 6th, and 7th rows the specific alert name differs from the general scanner name):
Alert_Name Source:PluginName WASC CWE
Cross Site Scripting (DOM Based) ACTIVE:Cross Site Scripting (DOM Based) 8 79
Non-Storable Content PASSIVE:Content Cacheability 13 524
Content Security Policy (CSP) Header Not Set PASSIVE:Content Security Policy (CSP) Header Not Set 15 16
Server Leaks Version Information via "Server" HTTP Response Header Field PASSIVE:HTTP Server Response Header Scanner 13 200
Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) PASSIVE:Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) 13 200
Non-Storable Content PASSIVE:Content Cacheability 13 524
Timestamp Disclosure - Unix PASSIVE:Timestamp Disclosure 13 200
Which pastes well in Excel:
Detailed steps:
(This assumes ZAP is running, and the session you want information for is open/loaded).
1. Goto the scripts tree (behind the Sites Tree) [if you can't see it
click the plus sign near the Sites Tree tab and add "Scripts"].
2. In the Scripts tree right click "Standalone" and select "New Script":
give it a name and select the JavaScript Script Engine ("EcmaScript
: Oracle Nashorn") [no Template is necessary]. Click "Save" on the New
Script dialog.
3. In the new script window (in the request/response area) paste the script
from the answer.
4. Run it (the blue triangle play button above the script
entry pane).
5. The results will display in the output pane below the
script.
6. Copy/paste the output into Excel.

Manipulate google form associated with a google sheet from app script in that sheet

I have a google spreadsheet which contains multiple sheets (or tabs) within it. Each sheet is populated from its own unique form. None of the forms are embedded in the spreadsheet.
Periodically, I need to delete all the data in the sheets, and also delete all the old responses which are saved in each of the forms. I can do this using a .gs script which resides in the spreadsheet. It accesses the form by its ID (the long string which appears in its URI). This requires the ID string to be hardcoded in my .gs script.
Ideally, I would like to access each form from the sheet object (i.e. the destination for each forms entries). Mock up code would look like this...
var ss = SpreadSheedApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var form = sheet.getMyAssociatedSourceForm(); // my dream method :-)
form.deleteAllResponses() // this method already exists
Does anyone know if this is possible? Or will I have to continue to use the ID (which is currently working)?
rgds...
I think you can do this without literally typing in ID's into your script. But, you would need to get every Form in your drive, loop through them all and get the destinationId() of every Form.
Google Documentation
Then compare the destinationId with the current spreadsheets ID, which you can get without needing to "hard code" it:
function deleteAllResponses() {
var thisSS_ID = SpreadsheetApp.getActiveSpreadsheet().getId();
var allForms = DriveApp.getFilesByType(MimeType.GOOGLE_FORMS);
var thisFormFile, thisFormFileID = "", thisForm, theDestID = "";
while (allForms.hasNext()) {
thisFormFile = allForms.next();
thisFormFileID = thisFormFile.getId();
thisForm = FormApp.openById(thisFormFileID);
try {
theDestID = thisForm.getDestinationId();
} catch(err) {
continue;
};
if (theDestID === "" || theDestID === undefined) {
continue;
};
if (theDestID === thisFormFileID) {
thisForm.deleteAllResponses();
};
};
};
I have not tested this, so don't know if it works. If it does, let me know in the comments section.

OneNote create Page with UpdateHierarchy - how to find new page?

I managed to create sections at very specific places within my OneNote Notebooks. Now I want to achieve the same with pages. So instead of using the unpredicteable-placing "CreateNewPage" method, I use UpdateHierarchy which works perfectly fine (for testing purpose I'm using AppendChild below).
The only issue I'm having is, that after creating the new page using UpdateHierarchy I'm completely loosing any links to the newly created page. OneNote assigns an ID and ignores all further Tags/Names I give. Also setting the One:T member used for setting the title is getting ignored - it always creates an "Untitled Page".
Am I doing anything wrong or do I need to first CreateNewPage and, using the assigned page-ID, re-place it using UpdateHierarchy?
Regards
Joel
function createPage {
param([string]$title, [string]$sectionnode)
[string]$pageref=$null
# Gather the pages within the notebook
[xml]$ref = $null
$_globalOneNote["COM"].GetHierarchy($sectionnode, [Microsoft.Office.InterOp.OneNote.HierarchyScope]::hsPages, [ref]$ref)
[System.Xml.XmlNamespaceManager] $nsmgr = $ref.NameTable
$nsmgr.AddNamespace('one', "http://schemas.microsoft.com/office/onenote/2010/onenote")
# Creation of a new page
$newPage = $ref.CreateElement('one', 'Page', 'http://schemas.microsoft.com/office/onenote/2010/onenote')
$newTitle = $ref.CreateElement('one', 'Title', 'http://schemas.microsoft.com/office/onenote/2010/onenote')
$newOE = $ref.CreateElement('one', 'OE', 'http://schemas.microsoft.com/office/onenote/2010/onenote')
$newT = $ref.CreateElement('one', 'T', 'http://schemas.microsoft.com/office/onenote/2010/onenote')
$newPage.SetAttribute('name', "Olololo")
$newT.InnerText = '<![CDATA[Testtitle]]>'
$newOE.AppendChild($newT)
$newTitle.AppendChild($newOE)
$newPage.AppendChild($newTitle)
$ref.Section.AppendChild($newPage)
$_globalBJBOneNote["COM"].UpdateHierarchy($ref.OuterXML)
}
This does the trick. Setting of title etc. must still be done using UpdatePageContent however the new page is placed properly. For putting metadata (title, indention etc.) a separate function may be used that works using the returned GUID of the createPage function.
function createPage {
param([string]$sectionnode, [string]$pagenode = $sectionnode)
# Gather the sections within the notebook
[xml]$ref = $null
$_globalBJBOneNote["COM"].GetHierarchy($sectionnode, [Microsoft.Office.InterOp.OneNote.HierarchyScope]::hsPages, [ref]$ref)
[System.Xml.XmlNamespaceManager] $nsmgr = $ref.NameTable
$nsmgr.AddNamespace('one', "http://schemas.microsoft.com/office/onenote/2010/onenote")
# Create new page
[string]$pageID = $null
$_globalBJBOneNote["COM"].createNewPage($ref.Section.ID, [ref]$pageID)
# Reload the hierarchy, now we can get the node of the new notebook
$_globalBJBOneNote["COM"].GetHierarchy($sectionnode, [Microsoft.Office.InterOp.OneNote.HierarchyScope]::hsPages, [ref]$ref)
$newPageNode = $ref.SelectSingleNode('//one:Page[#ID="' + $pageID + '"]', $nsmgr)
$referencePageNode = $ref.SelectSingleNode('//one:Page[#ID="' + $pagenode + '"]', $nsmgr)
# Reposition
[void]$ref.Section.removeChild($newPageNode)
[void]$ref.Section.InsertAfter($newPageNode, $referencePageNode)
$_globalBJBOneNote["COM"].UpdateHierarchy($ref.OuterXML)
$pageID
}

Moodle: Automating user/course creation and enrolments

I had a look at the documentation on enrolments, but all the enrolment methods seem to involve some interaction with the GUI.
Is there a way to script enrolments? Something like:
./moodle_do_enrolments imsdata.xml
Or even some web services calls that I can call from an external program?
I'd like to be able to do the following in an automated fashion:
1) Add a user.
2) Create a course with specified title etc.
3) Enrol that user in that course.
Of course at some point I'd hook this up with our user systems and other management systems, but for the moment, I'm just trying to do a proof of concept.
Where is some documentation that explains the process of automated enrolments?
You could try to create your own PHP script: parse the XML file and use internal moodle functions to solve the problem.
Basic ideas to solve these problems
1) Add a user:
In user/lib.php is a method: user_create_user($user).
Just include that lib.php and find out which information is needed in the user object.
2) Create a course
In course/lib.php is a method: create_course($data, $editoroptions).
Just include that lib.php and find out which information is needed in data array.
3) Enrol a user
I created the following method to do the job for me.
// enroll student to course (roleid = 5 is student role)
function enroll_to_course($courseid, $userid, $roleid=5, $extendbase=3, $extendperiod=0) {
global $DB;
$instance = $DB->get_record('enrol', array('courseid'=>$courseid, 'enrol'=>'manual'), '*', MUST_EXIST);
$course = $DB->get_record('course', array('id'=>$instance->courseid), '*', MUST_EXIST);
$today = time();
$today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0);
if(!$enrol_manual = enrol_get_plugin('manual')) { throw new coding_exception('Can not instantiate enrol_manual'); }
switch($extendbase) {
case 2:
$timestart = $course->startdate;
break;
case 3:
default:
$timestart = $today;
break;
}
if ($extendperiod <= 0) { $timeend = 0; } // extendperiod are seconds
else { $timeend = $timestart + $extendperiod; }
$enrolled = $enrol_manual->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
add_to_log($course->id, 'course', 'enrol', '../enrol/users.php?id='.$course->id, $course->id);
return $enrolled;
}
Using a GUI is not necessary, you can create an enrolment/authentication plugin to achieve this or use one of the built in ones. I'm not too familiar with the ims enrollment plugin, but the standard ldap/database plugins have scripts which can be used to automate this sync process.
See for example:
enrol/database/cli/sync.php