{"id":994,"date":"2023-05-09T08:51:57","date_gmt":"2023-05-09T15:51:57","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/powershell-community\/?p=994"},"modified":"2023-05-31T06:30:34","modified_gmt":"2023-05-31T13:30:34","slug":"porting-system-web-security-membership-generatepassword-to-powershell","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/powershell-community\/porting-system-web-security-membership-generatepassword-to-powershell\/","title":{"rendered":"Porting System.Web.Security.Membership.GeneratePassword() to PowerShell"},"content":{"rendered":"<p><!-- markdownlint-disable-file MD041 --><\/p>\n<p>I&#8217;ve been using PowerShell (core) for a couple of years now, and it became natural to create automations with all the features that are not present in Windows PowerShell. However, there is still one feature I miss in PowerShell, and this feature, for as silly as it sounds, is the <strong>GeneratePassword<\/strong>, from <strong>System.Web.Security.Membership<\/strong>.<\/p>\n<p>This happens because this assembly was developed in .NET Framework, and not brought to .NET (core). Although there are multiple alternatives to achieve the same result, I thought this is the perfect opportunity to show the Power in PowerShell, and port this method from C#.<\/p>\n<h2>Method<\/h2>\n<p>We are going to get this method&#8217;s code by using an IL decompiler. C# is compiled to an <strong>Intermediate Language<\/strong>, which allows us to decompile it. The tool I&#8217;ll be using is <code>ILSpy<\/code>, and can be found on the <a href=\"https:\/\/www.microsoft.com\/store\/productId\/9MXFBKFVSQ13\">Microsoft Store<\/a>.<\/p>\n<p><div class=\"alert alert-primary\"> The code for <strong>GeneratePassword<\/strong> and the <strong>System.Web<\/strong> library were not written by me, and the purpose of decompiling it is purely educational. For as harmless as this code is, it does not have any security warranties, nor is intended for misuse. <\/div><\/p>\n<h2>Getting the Code<\/h2>\n<p>Once installed, open <code>ILSpy<\/code>, click on <strong>File<\/strong> and <strong>Open from GAC&#8230;<\/strong>. On the search bar, type <strong>System.Web<\/strong>, select the assembly, and click <strong>Open<\/strong>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-content\/uploads\/sites\/69\/2023\/05\/File-OpenFromGAC-1.png\" alt=\"File menu\" \/> <img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-content\/uploads\/sites\/69\/2023\/05\/OpenFromGACMenu-1.png\" alt=\"Open from GAC menu\" \/><\/p>\n<p>Once loaded, expand the <strong>System.Web<\/strong> assembly tree, and the <strong>System.Web.Security<\/strong> namespace. Inside <strong>System.Web.Security<\/strong>, look for the <strong>Membership<\/strong> class, click on it, and the decompiled code should appear on the right pane.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-content\/uploads\/sites\/69\/2023\/05\/MembershipClass-1.png\" alt=\"Membership class\" \/><\/p>\n<p>Scroll down until you find the <strong>GeneratePassword<\/strong> method, and expand it.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-content\/uploads\/sites\/69\/2023\/05\/GeneratePasswordMethod-1.png\" alt=\"GeneratePassword method\" \/><\/p>\n<h2>Porting to PowerShell<\/h2>\n<p>Now the fun begins. Let&#8217;s do this using PowerShell tools only, means we&#8217;re not going to copy the <strong>Membership<\/strong> class and method. We are going to create a function, and keep the variable names the same, so it&#8217;s easier for us to compare.<\/p>\n<ul>\n<li>Starting with the method&#8217;s signature: <code>public static string GeneratePassword(int lenght, int numberOfNonAlphanumericCharacters)<\/code> \n<ul>\n<li><strong>public<\/strong> means this method can be called from outside the assembly.<\/li>\n<li><strong>static<\/strong> means I can call this method without having to instantiate an object of type <strong>Membership<\/strong>.<\/li>\n<li><strong>string<\/strong> means this method returns a string.<\/li>\n<\/ul>\n<\/li>\n<li>Utility methods and properties. <strong>GeneratePassword<\/strong> uses methods and properties that are also defined in the <strong>System.Web<\/strong> library. \n<ul>\n<li>Methods<\/li>\n<li><code>System.Web.CrossSiteScriptingValidation.IsDangerousString(string s, out int matchIndex)<\/code><\/li>\n<li><code>System.Web.CrossSiteScriptingValidation.IsAtoZ(char c)<\/code><\/li>\n<li>Properties<\/li>\n<li><code>char[] punctuations<\/code>, from <strong>System.Web.Security.Membership<\/strong><\/li>\n<li><code>char[] startingChars<\/code>, from <strong>System.Web.CrossSiteScriptingValidation<\/strong><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>Now enough C#, let get to scripting.<\/p>\n<h3>Main function<\/h3>\n<p>For this, we are going to use the <strong>Advanced Function<\/strong> template, from Visual Studio Code. I&#8217;ll name the main function <code>New-StrongPassword<\/code>, but you can name it as you like, just remember using approved verbs.<\/p>\n<p>This method takes as parameter two integer numbers, let&#8217;s create them in the <code>param()<\/code> block. The first two <code>if<\/code> statements are checks to ensure both parameters are within acceptable range. We can accomplish the same with parameter attributes.<\/p>\n<pre><code class=\"language-powershell\">function New-StrongPassword {\n\n    [CmdletBinding()]\n    param (\n\n        # Number of characters.\n        [Parameter(\n            Mandatory,\n            Position = 0,\n            HelpMessage = 'The number of characters the password should have.'\n        )]\n        [ValidateRange(1, 128)]\n        [int] $Length,\n\n        # Number of non alpha-numeric chars.\n        [Parameter(\n            Mandatory,\n            Position = 1,\n            HelpMessage = 'The number of non alpha-numeric characters the password should contain.'\n        )]\n        [ValidateScript({\n            if ($PSItem -gt $Length -or $PSItem -lt 0) {\n                $newObjectSplat = @{\n                    TypeName = 'System.ArgumentException'\n                    ArgumentList = 'Membership minimum required non alpha-numeric characters is incorrect'\n                }\n                throw New-Object @newObjectSplat\n            }\n            return $true\n        })]\n        [int] $NumberOfNonAlphaNumericCharacters\n\n    )\n\n    begin {\n\n    }\n\n    process {\n\n    }\n\n    end {\n\n    }\n}<\/code><\/pre>\n<h3>Utilities<\/h3>\n<p>Now let&#8217;s focus on the <code>Begin{}<\/code> block, and create those utility methods, and properties.<\/p>\n<h4>Properties<\/h4>\n<p>These are the two properties, in our case variables, that we need to create.<\/p>\n<pre><code class=\"language-csharp\">private static char[] startingChars = new char[2] { '&lt;', '&' };\nprivate static char[] punctuations = \"!@#$%^&*()_-+=[{]};:&gt;|.\/?\".ToCharArray();<\/code><\/pre>\n<p>Let&#8217;s create them as global variables, to be used across our functions if necessary.<\/p>\n<pre><code class=\"language-powershell\">[char[]]$global:punctuations = @('!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_',\n                                 '-', '+', '=', '[', '{', ']', '}', ';', ':', '&gt;', '|',\n                                 '.', '\/', '?')\n[char[]]$global:startingChars = @('&lt;', '&')<\/code><\/pre>\n<h4>Get-IsAtoZ<\/h4>\n<p>This is what the method looks like:<\/p>\n<pre><code class=\"language-csharp\">private static bool IsAtoZ(char c)\n{\n    if (c &lt; 'a' || c &gt; 'z')\n    {\n        if (c &gt;= 'A')\n        {\n            return c &lt;= 'Z';\n        }\n        return false;\n    }\n    return true;\n}<\/code><\/pre>\n<p>Pretty simple method, with one parameter, only the operator&#8217;s name needs to change. Let&#8217;s use an inline function:<\/p>\n<pre><code class=\"language-powershell\">function Get-IsAToZ([char]$c) {\n    if ($c -lt 'a' -or $c -gt 'z') {\n        if ($c -ge 'A') {\n            return $c -le 'Z'\n        }\n        return $false\n    }\n    return $true\n}<\/code><\/pre>\n<h4>Get-IsDangerousString<\/h4>\n<p>This is what the C# method looks like:<\/p>\n<pre><code class=\"language-csharp\">internal static bool IsDangerousString(string s, out int matchIndex)\n{\n    matchIndex = 0;\n    int startIndex = 0;\n    while (true)\n    {\n        int num = s.IndexOfAny(startingChars, startIndex);\n        if (num &lt; 0)\n        {\n            return false;\n        }\n        if (num == s.Length - 1)\n        {\n            break;\n        }\n        matchIndex = num;\n        switch (s[num])\n        {\n        case '&lt;':\n            if (IsAtoZ(s[num + 1]) || s[num + 1] == '!' || s[num + 1] == '\/' || s[num + 1] == '?')\n            {\n                return true;\n            }\n            break;\n        case '&':\n            if (s[num + 1] == '#')\n            {\n                return true;\n            }\n            break;\n        }\n        startIndex = num + 1;\n    }\n    return false;\n}<\/code><\/pre>\n<p>This one is a little more extensive, but it&#8217;s pretty much only string manipulation. The interesting part of this method though, is the parameter <strong>matchIndex<\/strong>. Note the <code>out<\/code> keyword, this means this parameter is passed as reference. We could skip this parameter altogether, because is not used in our case, but this is a perfect opportunity to exercise the <strong>PSReference<\/strong> type.<\/p>\n<pre><code class=\"language-powershell\">function Get-IsDangerousString {\n\n    param([string]$s, [ref]$matchIndex)\n\n    # To access the referenced parameter's value, we use the 'Value' property from PSReference.\n    $matchIndex.Value = 0\n    $startIndex = 0\n\n    while ($true) {\n        $num = $s.IndexOfAny($global:startingChars, $startIndex)\n        if ($num -lt 0) {\n            return $false\n        }\n        if ($num -eq $s.Length - 1) {\n            break\n        }\n        $matchIndex.Value = $num\n\n        switch ($s[$num]) {\n            '&lt;' {\n                if (\n                    (Get-IsAToZ($s[$num + 1])) -or\n                    ($s[$num + 1] -eq '!')     -or\n                    ($s[$num + 1] -eq '\/')     -or\n                    ($s[$num + 1] -eq '?')\n                ) {\n                    return $true\n                }\n            }\n            '&' {\n                if ($s[$num + 1] -eq '#') {\n                    return $true\n                }\n            }\n        }\n        $startIndex = $num + 1\n    }\n    return $false\n}<\/code><\/pre>\n<p>With these, our <code>Begin{}<\/code> block looks like this:<\/p>\n<pre><code class=\"language-powershell\">Begin {\n    [char[]]$global:punctuations = @('!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_',\n                                     '-', '+', '=', '[', '{', ']', '}', ';', ':', '&gt;', '|',\n                                     '.', '\/', '?')\n    [char[]]$global:startingChars = @('&lt;', '&')\n\n    function Get-IsAToZ([char]$c) {\n        if ($c -lt 'a' -or $c -gt 'z') {\n            if ($c -ge 'A') {\n                return $c -le 'Z'\n            }\n            return $false\n        }\n        return $true\n    }\n\n    function Get-IsDangerousString {\n\n        param([string]$s, [ref]$matchIndex)\n\n        $matchIndex.Value = 0\n        $startIndex = 0\n\n        while ($true) {\n            $num = $s.IndexOfAny($global:startingChars, $startIndex)\n            if ($num -lt 0) {\n                return $false\n            }\n            if ($num -eq $s.Length - 1) {\n                break\n            }\n            $matchIndex.Value = $num\n\n            switch ($s[$num]) {\n                '&lt;' {\n                    if (\n                        (Get-IsAToZ($s[$num + 1])) -or\n                        ($s[$num + 1] -eq '!')     -or\n                        ($s[$num + 1] -eq '\/')     -or\n                        ($s[$num + 1] -eq '?')\n                    ) {\n                        return $true\n                    }\n                }\n                '&' {\n                    if ($s[$num + 1] -eq '#') {\n                        return $true\n                    }\n                }\n            }\n            $startIndex = $num + 1\n        }\n        return $false\n    }\n}<\/code><\/pre>\n<h3>Main Function Body<\/h3>\n<p>In this stage we build the function itself. Since we&#8217;re using attributes to check the parameters, the first two <code>if<\/code> statements are ignored. After that, we have a single <code>do-while<\/code> loop. In this loop, we are going to use tools from the <strong>System.Security.Cryptography<\/strong> library, so let&#8217;s import it.<\/p>\n<pre><code class=\"language-powershell\">Add-Type -AssemblyName System.Security.Cryptography\n\n# If you get 'Assembly cannot be found' errors, load it with partial name instead.\n[void][System.Reflection.Assembly]::LoadWithPartialName('System.Security.Cryptography')<\/code><\/pre>\n<p>First let&#8217;s declare the variables used in the main function body, and inside the main loop. This gives us the opportunity to analyze our choices.<\/p>\n<pre><code class=\"language-powershell\"># Explicitly declaring the output 'text' to match the method. We can skip this delaration.\n# Same for the 'matchIndex'\n$text = [string]::Empty\n$matchIndex = 0\ndo {\n    $array = New-Object -TypeName 'System.Byte[]' -ArgumentList $Length\n    $array2 = New-Object -TypeName 'System.Char[]' -ArgumentList $Length\n    $num = 0\n\n    # This stage could be done in 3 ways. We could use 'New-Object' and imediately call\n    # 'GetBytes' on it, we could use the class constructor directly, and call 'GetBytes'\n    # on it: [System.Security.Cryptography.RNGCryptoServiceProvider]::new().GetBytes(),\n    # or we could instantiate the 'RNGCryptoServiceProvider' object using one of the\n    # previous methods, and call 'GetBytes' on it. Since we're using PowerShell tools the\n    # most we can, and we want to stay true to the method, let's use the first option.\n    # [void] used to suppress output.\n    [void](New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider').GetBytes($array)\n\n    # Note that when passing a variable as reference to a function parameter, we need to\n    # cast it to 'PSReference'. The parentheses are necessary so the parameter uses the\n    # object, and not use it as a string.\n} while ((Get-IsDangerousString -s $text -matchIndex ([ref]$matchIndex)))<\/code><\/pre>\n<p>Note that in our pursuit to stay true to the method&#8217;s layout, we are including extra declarations. Although this could be avoided, in some cases it helps with script readability. Plus, if you have experience with any programming language, this will feel familiar.<\/p>\n<p>Right after that, we have a <code>for<\/code> loop, which will choose each character for our password. It does this with a series of mathematical operations, and comparisons.<\/p>\n<pre><code class=\"language-powershell\">for ($i = 0; $i -lt $Length; $i++) {\n    $num2 = [int]$array[$i] % 87\n    if ($num2 -lt 10) {\n        $array2[$i] = [char](48 + $num2)\n        continue\n    }\n    if ($num2 -lt 36) {\n        $array2[$i] = [char](65 + $num2 - 10)\n        continue\n    }\n    if ($num2 -lt 62) {\n        $array2[$i] = [char](97 + $num2 - 36)\n        continue\n    }\n    $array2[$i] = $global:punctuations[$num2 - 62]\n    $num++\n}<\/code><\/pre>\n<p>The next session is going to manage our number of non-alphanumeric characters. It does that by generating random symbol characters and replacing values in the array we filled in the previous loop.<\/p>\n<pre><code class=\"language-powershell\">if ($num -lt $NumberOfNonAlphaNumericCharacters) {\n    $random = New-Object -TypeName 'System.Random'\n\n    # Generating only the characters left to complete our parameter specification.\n    for ($j = 0; $j -lt $NumberOfNonAlphaNumericCharacters - $num; $j++) {\n        $num3 = 0\n        do {\n            $num3 = $random.Next(0, $Length)\n        } while (![char]::IsLetterOrDigit($array2[$num3]))\n        $array2[$num3] = $global:punctuations[$random.Next(0, $global:punctuations.Length)]\n    }\n}<\/code><\/pre>\n<p>Now all that&#8217;s left is to create a string from the character array, and check if it&#8217;s safe with <code>Get-IsDangerousString<\/code>.<\/p>\n<pre><code class=\"language-powershell\">$text = [string]::new($array2)<\/code><\/pre>\n<p>If our <code>text<\/code> is safe, we return it and the function reaches end of execution. Our finished function looks like this:<\/p>\n<pre><code class=\"language-powershell\">function New-StrongPassword {\n\n    [CmdletBinding()]\n    param (\n\n        # Number of characters.\n        [Parameter(\n            Mandatory,\n            Position = 0,\n            HelpMessage = 'The number of characters the password should have.'\n        )]\n        [ValidateRange(1, 128)]\n        [int] $Length,\n\n        # Number of non alpha-numeric chars.\n        [Parameter(\n            Mandatory,\n            Position = 1,\n            HelpMessage = 'The number of non alpha-numeric characters the password should contain.'\n        )]\n        [ValidateScript({\n            if ($PSItem -gt $Length -or $PSItem -lt 0) {\n                $newObjectSplat = @{\n                    TypeName = 'System.ArgumentException'\n                    ArgumentList = 'Membership minimum required non alpha-numeric characters is incorrect'\n                }\n                throw New-Object @newObjectSplat\n            }\n        })]\n        [int] $NumberOfNonAlphaNumericCharacters\n\n    )\n\n    Begin {\n        [char[]]$global:punctuations = @('!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_',\n                                         '-', '+', '=', '[', '{', ']', '}', ';', ':', '&gt;', '|',\n                                         '.', '\/', '?')\n        [char[]]$global:startingChars = @('&lt;', '&')\n\n        function Get-IsAToZ([char]$c) {\n            if ($c -lt 'a' -or $c -gt 'z') {\n                if ($c -ge 'A') {\n                    return $c -le 'Z'\n                }\n                return $false\n            }\n            return $true\n        }\n\n        function Get-IsDangerousString {\n\n            param([string]$s, [ref]$matchIndex)\n\n            $matchIndex.Value = 0\n            $startIndex = 0\n\n            while ($true) {\n                $num = $s.IndexOfAny($global:startingChars, $startIndex)\n                if ($num -lt 0) {\n                    return $false\n                }\n                if ($num -eq $s.Length - 1) {\n                    break\n                }\n                $matchIndex.Value = $num\n\n                switch ($s[$num]) {\n                    '&lt;' {\n                        if (\n                            (Get-IsAToZ($s[$num + 1])) -or\n                            ($s[$num + 1] -eq '!')     -or\n                            ($s[$num + 1] -eq '\/')     -or\n                            ($s[$num + 1] -eq '?')\n                        ) {\n                            return $true\n                        }\n                    }\n                    '&' {\n                        if ($s[$num + 1] -eq '#') {\n                            return $true\n                        }\n                    }\n                }\n                $startIndex = $num + 1\n            }\n            return $false\n        }\n    }\n\n    Process {\n        Add-Type -AssemblyName 'System.Security.Cryptography'\n\n        $text = [string]::Empty\n        $matchIndex = 0\n        do {\n            $array = New-Object -TypeName 'System.Byte[]' -ArgumentList $Length\n            $array2 = New-Object -TypeName 'System.Char[]' -ArgumentList $Length\n            $num = 0\n            [void](New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider').GetBytes($array)\n\n            for ($i = 0; $i -lt $Length; $i++) {\n                $num2 = [int]$array[$i] % 87\n                if ($num2 -lt 10) {\n                    $array2[$i] = [char](48 + $num2)\n                    continue\n                }\n                if ($num2 -lt 36) {\n                    $array2[$i] = [char](65 + $num2 - 10)\n                    continue\n                }\n                if ($num2 -lt 62) {\n                    $array2[$i] = [char](97 + $num2 - 36)\n                    continue\n                }\n                $array2[$i] = $global:punctuations[$num2 - 62]\n                $num++\n            }\n\n            if ($num -lt $NumberOfNonAlphaNumericCharacters) {\n                $random = New-Object -TypeName 'System.Random'\n\n                for ($j = 0; $j -lt $NumberOfNonAlphaNumericCharacters - $num; $j++) {\n                    $num3 = 0\n                    do {\n                        $num3 = $random.Next(0, $Length)\n                    } while (![char]::IsLetterOrDigit($array2[$num3]))\n                    $array2[$num3] = $global:punctuations[$random.Next(0, $global:punctuations.Length)]\n                }\n            }\n\n            $text = [string]::new($array2)\n        } while ((Get-IsDangerousString -s $text -matchIndex ([ref]$matchIndex)))\n    }\n\n    End {\n        return $text\n    }\n}<\/code><\/pre>\n<h3>Result<\/h3>\n<p>Now all that&#8217;s left is to call our function:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-content\/uploads\/sites\/69\/2023\/05\/Result-1.png\" alt=\"New-StrongPassword\" \/><\/p>\n<h2>Conclusion<\/h2>\n<p>I hope you had as much fun as I had building this function. With this new skill, you can improve your scripts&#8217; complexity and reliability. This also makes you more comfortable to write your own modules, binary or not.<\/p>\n<p>Thank you for going along.<\/p>\n<p>Happy scripting!<\/p>\n<h2>Links<\/h2>\n<ul>\n<li><a href=\"https:\/\/github.com\/icsharpcode\/ILSpy\">ILSpy GitHub page<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/FranciscoNabas\/WindowsUtils\">Test our WindowsUtils module!<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/FranciscoNabas\">See what I&#8217;m up to<\/a><\/li>\n<\/ul>\n<p><!-- link references --><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post shows how to port a C# method into PowerShell<\/p>\n","protected":false},"author":62334,"featured_media":77,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[13,2],"tags":[83,89,87,88,3],"class_list":["post-994","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-powershell","category-powershell-community","tag-automation","tag-c","tag-password","tag-portability","tag-powershell"],"acf":[],"blog_post_summary":"<p>This post shows how to port a C# method into PowerShell<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/posts\/994","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/users\/62334"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/comments?post=994"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/posts\/994\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/media\/77"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/media?parent=994"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/categories?post=994"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/powershell-community\/wp-json\/wp\/v2\/tags?post=994"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}