@kyverno/backstage-plugin-policy-reporter-backend 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @kyverno/backstage-plugin-policy-reporter-backend
2
2
 
3
+ ## 2.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fa25ccc: Added additional responses to all routes, they can now also return 404, 502 and 503
8
+ - 256fb9a: Added source filtering to the PolicyReportsPage. Users can now filter the list of policy reports by selecting one or more sources from the selected cluster.
9
+ - 2f4ac45: Added policy filtering to the PolicyReportsPage. Users can now filter the list of policy reports by selecting one or more policies from the selected cluster.
10
+ - d598ade: Added category filtering to the PolicyReportsPage. Users can now filter the list of policy reports by selecting one or more categories from the selected cluster.
11
+ - a664d0c: Added kind filtering to the PolicyReportsPage. Users can now filter the list of policy reports by selecting one or more kinds from the selected cluster.
12
+ - 880eecc: Backstage version bump to v1.51.1
13
+ - fa25ccc: Add support for configuring requestHeaders for the Policy Reporter Backend in your config using policyReporter.requestHeaders. This can be used for configuring Basic Auth credentials to the backend.
14
+
15
+ ```yaml
16
+ policyReporter:
17
+ requestHeaders:
18
+ Authorization: Bearer ${POLICY_REPORTER_API_TOKEN}
19
+ ```
20
+
21
+ - 44c5238: Backstage version bump to v1.52.0
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies [256fb9a]
26
+ - Updated dependencies [2f4ac45]
27
+ - Updated dependencies [d598ade]
28
+ - Updated dependencies [a664d0c]
29
+ - Updated dependencies [880eecc]
30
+ - Updated dependencies [44c5238]
31
+ - @kyverno/backstage-plugin-policy-reporter-common@2.5.0
32
+
33
+ ## 2.5.0
34
+
35
+ ### Minor Changes
36
+
37
+ - e0e8462: Backstage verion bump to v1.50.4
38
+
39
+ ### Patch Changes
40
+
41
+ - Updated dependencies [e0e8462]
42
+ - @kyverno/backstage-plugin-policy-reporter-common@2.4.0
43
+
3
44
  ## 2.4.0
4
45
 
5
46
  ### Minor Changes
package/config.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface Config {
2
+ policyReporter?: {
3
+ /**
4
+ * Optional HTTP headers forwarded with every outbound request to every Policy Reporter API
5
+ * Typically used to authenticate through a gateway/proxy.
6
+ * @visibility backend
7
+ */
8
+ requestHeaders?: { [key: string]: string };
9
+ };
10
+ }
package/dist/index.d.ts CHANGED
@@ -1,13 +1,180 @@
1
1
  import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
- import { LoggerService, RootConfigService, AuthService } from '@backstage/backend-plugin-api';
2
+ import { LoggerService, RootConfigService } from '@backstage/backend-plugin-api';
3
3
  import express from 'express';
4
- import { CatalogService } from '@backstage/plugin-catalog-node';
4
+
5
+ /**
6
+ * @public
7
+ */
8
+ type Direction = 'asc' | 'desc';
9
+
10
+ /**
11
+ * @public
12
+ */
13
+ type Severity = 'unknown' | 'info' | 'low' | 'medium' | 'high' | 'critical';
14
+
15
+ /**
16
+ * @public
17
+ */
18
+ type Status = 'fail' | 'skip' | 'pass' | 'warn' | 'error' | 'summary';
19
+
20
+ /**
21
+ * Filter options for policy results
22
+ * @public
23
+ */
24
+ interface Filter {
25
+ /**
26
+ * Filter by a list of sources
27
+ */
28
+ sources?: Array<string>;
29
+ /**
30
+ * Filter by a list of namespaces
31
+ */
32
+ namespaces?: Array<string>;
33
+ /**
34
+ * Filter by a list of kinds
35
+ */
36
+ kinds?: Array<string>;
37
+ /**
38
+ * Filter by a list of resources
39
+ */
40
+ resources?: Array<string>;
41
+ /**
42
+ * Filter by a list of categories
43
+ */
44
+ categories?: Array<string>;
45
+ /**
46
+ * Filter by a list of policies
47
+ */
48
+ policies?: Array<string>;
49
+ /**
50
+ * Filter by a list of status
51
+ */
52
+ status?: Array<Status>;
53
+ /**
54
+ * Filter by a list of severities
55
+ */
56
+ severities?: Array<Severity>;
57
+ /**
58
+ * Filter by search string
59
+ */
60
+ search?: string;
61
+ /**
62
+ * Filter by label-value pairs
63
+ */
64
+ labels?: Array<string>;
65
+ }
66
+
67
+ /**
68
+ * @public
69
+ */
70
+ interface ListResult {
71
+ /**
72
+ * Unique identifier for the result
73
+ */
74
+ id: string;
75
+ /**
76
+ * Kubernetes namespace
77
+ */
78
+ namespace: string;
79
+ /**
80
+ * Kubernetes resource kind
81
+ */
82
+ kind: string;
83
+ /**
84
+ * Resource name
85
+ */
86
+ name: string;
87
+ /**
88
+ * Unique resource identifier
89
+ */
90
+ resourceId: string;
91
+ /**
92
+ * Policy result message
93
+ */
94
+ message: string;
95
+ /**
96
+ * Policy name
97
+ */
98
+ policy: string;
99
+ /**
100
+ * Policy rule name
101
+ */
102
+ rule: string;
103
+ status: Status;
104
+ severity: Severity;
105
+ /**
106
+ * Unix timestamp of the result
107
+ */
108
+ timestamp: number;
109
+ /**
110
+ * Additional key-value properties
111
+ */
112
+ properties: {
113
+ [key: string]: string;
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Pagination options for results
119
+ * @public
120
+ */
121
+ interface Pagination {
122
+ /**
123
+ * Requested list page number
124
+ */
125
+ page?: number;
126
+ /**
127
+ * Number of results per page
128
+ */
129
+ offset?: number;
130
+ /**
131
+ * Fields to sort by
132
+ */
133
+ sortBy?: Array<string>;
134
+ direction?: Direction;
135
+ }
136
+
137
+ /**
138
+ * @public
139
+ */
140
+ interface ResultList {
141
+ items: Array<ListResult>;
142
+ /**
143
+ * Total number of results
144
+ */
145
+ count: number;
146
+ }
147
+
148
+ interface PolicyReporterApi {
149
+ getNamespacedResourceResults(options: {
150
+ entityRef: string;
151
+ query: Filter & Pagination;
152
+ }): Promise<ResultList>;
153
+ getNamespaces(options: {
154
+ entityRef: string;
155
+ query: Pick<Filter, 'sources' | 'categories' | 'policies'>;
156
+ }): Promise<string[]>;
157
+ getSources(options: {
158
+ entityRef: string;
159
+ }): Promise<string[]>;
160
+ getKinds(options: {
161
+ entityRef: string;
162
+ query: Pick<Filter, 'sources' | 'kinds'>;
163
+ }): Promise<string[]>;
164
+ getCategories(options: {
165
+ entityRef: string;
166
+ query: Pick<Filter, 'sources' | 'namespaces'>;
167
+ }): Promise<string[]>;
168
+ getPolicies(options: {
169
+ entityRef: string;
170
+ query: Pick<Filter, 'sources' | 'namespaces' | 'categories'>;
171
+ }): Promise<string[]>;
172
+ }
5
173
 
6
174
  interface RouterOptions {
7
175
  logger: LoggerService;
8
176
  config: RootConfigService;
9
- catalogService: CatalogService;
10
- authService: AuthService;
177
+ policyReporterService: PolicyReporterApi;
11
178
  }
12
179
  declare function createRouter(options: RouterOptions): Promise<express.Router>;
13
180
 
@@ -3,6 +3,7 @@
3
3
  var backendPluginApi = require('@backstage/backend-plugin-api');
4
4
  var router = require('./service/router.cjs.js');
5
5
  var pluginCatalogNode = require('@backstage/plugin-catalog-node');
6
+ var policyReporterService = require('./service/policyReporterService.cjs.js');
6
7
 
7
8
  const kyvernoPolicyReportsPlugin = backendPluginApi.createBackendPlugin({
8
9
  pluginId: "policy-reporter",
@@ -16,12 +17,17 @@ const kyvernoPolicyReportsPlugin = backendPluginApi.createBackendPlugin({
16
17
  authService: backendPluginApi.coreServices.auth
17
18
  },
18
19
  async init({ httpRouter, logger, config, catalogService, authService }) {
20
+ const policyReporterService$1 = new policyReporterService.PolicyReporterService({
21
+ logger,
22
+ configService: config,
23
+ catalogService,
24
+ authService
25
+ });
19
26
  httpRouter.use(
20
27
  await router.createRouter({
21
28
  logger,
22
29
  config,
23
- catalogService,
24
- authService
30
+ policyReporterService: policyReporterService$1
25
31
  })
26
32
  );
27
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\n\n/**\n * kyvernoPolicyReportsPlugin backend plugin\n *\n * @public\n */\nexport const kyvernoPolicyReportsPlugin = createBackendPlugin({\n pluginId: 'policy-reporter',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n catalogService: catalogServiceRef,\n authService: coreServices.auth,\n },\n async init({ httpRouter, logger, config, catalogService, authService }) {\n httpRouter.use(\n await createRouter({\n logger,\n config,\n catalogService,\n authService,\n }),\n );\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","catalogServiceRef","createRouter"],"mappings":";;;;;;AAYO,MAAM,6BAA6BA,oCAAoB,CAAA;AAAA,EAC5D,QAAU,EAAA,iBAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,YAAYC,6BAAa,CAAA,UAAA;AAAA,QACzB,QAAQA,6BAAa,CAAA,MAAA;AAAA,QACrB,QAAQA,6BAAa,CAAA,UAAA;AAAA,QACrB,cAAgB,EAAAC,mCAAA;AAAA,QAChB,aAAaD,6BAAa,CAAA;AAAA,OAC5B;AAAA,MACA,MAAM,KAAK,EAAE,UAAA,EAAY,QAAQ,MAAQ,EAAA,cAAA,EAAgB,aAAe,EAAA;AACtE,QAAW,UAAA,CAAA,GAAA;AAAA,UACT,MAAME,mBAAa,CAAA;AAAA,YACjB,MAAA;AAAA,YACA,MAAA;AAAA,YACA,cAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AAAA;AACF,KACD,CAAA;AAAA;AAEL,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './service/router';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport { PolicyReporterService } from './service/policyReporterService';\n\n/**\n * kyvernoPolicyReportsPlugin backend plugin\n *\n * @public\n */\nexport const kyvernoPolicyReportsPlugin = createBackendPlugin({\n pluginId: 'policy-reporter',\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n catalogService: catalogServiceRef,\n authService: coreServices.auth,\n },\n async init({ httpRouter, logger, config, catalogService, authService }) {\n const policyReporterService = new PolicyReporterService({\n logger,\n configService: config,\n catalogService,\n authService,\n });\n\n httpRouter.use(\n await createRouter({\n logger,\n config,\n policyReporterService,\n }),\n );\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","catalogServiceRef","policyReporterService","PolicyReporterService","createRouter"],"mappings":";;;;;;;AAaO,MAAM,6BAA6BA,oCAAA,CAAoB;AAAA,EAC5D,QAAA,EAAU,iBAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,cAAA,EAAgBC,mCAAA;AAAA,QAChB,aAAaD,6BAAA,CAAa;AAAA,OAC5B;AAAA,MACA,MAAM,KAAK,EAAE,UAAA,EAAY,QAAQ,MAAA,EAAQ,cAAA,EAAgB,aAAY,EAAG;AACtE,QAAA,MAAME,uBAAA,GAAwB,IAAIC,2CAAA,CAAsB;AAAA,UACtD,MAAA;AAAA,UACA,aAAA,EAAe,MAAA;AAAA,UACf,cAAA;AAAA,UACA;AAAA,SACD,CAAA;AAED,QAAA,UAAA,CAAW,GAAA;AAAA,UACT,MAAMC,mBAAA,CAAa;AAAA,YACjB,MAAA;AAAA,YACA,MAAA;AAAA,mCACAF;AAAA,WACD;AAAA,SACH;AAAA,MACF;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
@@ -215,6 +215,280 @@ const spec = {
215
215
  }
216
216
  }
217
217
  },
218
+ "/v1/namespaced-resources/sources": {
219
+ get: {
220
+ summary: "Get sources",
221
+ description: "List of all Sources with namespace scoped results",
222
+ operationId: "getSources",
223
+ parameters: [
224
+ {
225
+ name: "environment",
226
+ in: "query",
227
+ required: true,
228
+ description: "The environment entity reference (URL encoded)",
229
+ schema: {
230
+ type: "string",
231
+ example: "default/component/my-service"
232
+ }
233
+ }
234
+ ],
235
+ responses: {
236
+ "200": {
237
+ description: "List of sources",
238
+ content: {
239
+ "application/json": {
240
+ schema: {
241
+ type: "array",
242
+ items: {
243
+ type: "string"
244
+ }
245
+ }
246
+ }
247
+ }
248
+ },
249
+ "400": {
250
+ description: "Bad request - invalid entityRef or missing annotation",
251
+ content: {
252
+ "application/json": {
253
+ schema: {
254
+ $ref: "#/components/schemas/RequestError"
255
+ }
256
+ }
257
+ }
258
+ },
259
+ "500": {
260
+ description: "Internal server error",
261
+ content: {
262
+ "application/json": {
263
+ schema: {
264
+ $ref: "#/components/schemas/RequestError"
265
+ }
266
+ }
267
+ }
268
+ },
269
+ "502": {
270
+ description: "Bad Gateway",
271
+ content: {
272
+ "application/json": {
273
+ schema: {
274
+ $ref: "#/components/schemas/RequestError"
275
+ }
276
+ }
277
+ }
278
+ },
279
+ "503": {
280
+ description: "Service Unavailable",
281
+ content: {
282
+ "application/json": {
283
+ schema: {
284
+ $ref: "#/components/schemas/RequestError"
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ },
292
+ "/v1/namespaced-resources/kinds": {
293
+ get: {
294
+ summary: "Get kinds",
295
+ description: "List of all Kubernetes resource kinds with namespace scoped results",
296
+ operationId: "getKinds",
297
+ parameters: [
298
+ {
299
+ name: "environment",
300
+ in: "query",
301
+ required: true,
302
+ description: "The environment entity reference (URL encoded)",
303
+ schema: {
304
+ type: "string",
305
+ example: "default/component/my-service"
306
+ }
307
+ },
308
+ {
309
+ name: "sources",
310
+ in: "query",
311
+ description: "Filter by a list of sources",
312
+ schema: {
313
+ type: "array",
314
+ items: {
315
+ type: "string"
316
+ }
317
+ },
318
+ style: "form",
319
+ explode: true
320
+ },
321
+ {
322
+ name: "namespaces",
323
+ in: "query",
324
+ description: "Filter by a list of namespaces",
325
+ schema: {
326
+ type: "array",
327
+ items: {
328
+ type: "string"
329
+ }
330
+ },
331
+ style: "form",
332
+ explode: true
333
+ }
334
+ ],
335
+ responses: {
336
+ "200": {
337
+ description: "List of kinds",
338
+ content: {
339
+ "application/json": {
340
+ schema: {
341
+ type: "array",
342
+ items: {
343
+ type: "string"
344
+ }
345
+ }
346
+ }
347
+ }
348
+ },
349
+ "400": {
350
+ description: "Bad request - invalid entityRef or missing annotation",
351
+ content: {
352
+ "application/json": {
353
+ schema: {
354
+ $ref: "#/components/schemas/RequestError"
355
+ }
356
+ }
357
+ }
358
+ },
359
+ "500": {
360
+ description: "Internal server error",
361
+ content: {
362
+ "application/json": {
363
+ schema: {
364
+ $ref: "#/components/schemas/RequestError"
365
+ }
366
+ }
367
+ }
368
+ },
369
+ "502": {
370
+ description: "Bad Gateway",
371
+ content: {
372
+ "application/json": {
373
+ schema: {
374
+ $ref: "#/components/schemas/RequestError"
375
+ }
376
+ }
377
+ }
378
+ },
379
+ "503": {
380
+ description: "Service Unavailable",
381
+ content: {
382
+ "application/json": {
383
+ schema: {
384
+ $ref: "#/components/schemas/RequestError"
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }
391
+ },
392
+ "/v1/namespaced-resources/categories": {
393
+ get: {
394
+ summary: "Get categories",
395
+ description: "List of all Categories with namespace scoped results",
396
+ operationId: "getCategories",
397
+ parameters: [
398
+ {
399
+ name: "environment",
400
+ in: "query",
401
+ required: true,
402
+ description: "The environment entity reference (URL encoded)",
403
+ schema: {
404
+ type: "string",
405
+ example: "default/component/my-service"
406
+ }
407
+ },
408
+ {
409
+ name: "sources",
410
+ in: "query",
411
+ description: "Filter by a list of sources",
412
+ schema: {
413
+ type: "array",
414
+ items: {
415
+ type: "string"
416
+ }
417
+ },
418
+ style: "form",
419
+ explode: true
420
+ },
421
+ {
422
+ name: "namespaces",
423
+ in: "query",
424
+ description: "Filter by a list of namespaces",
425
+ schema: {
426
+ type: "array",
427
+ items: {
428
+ type: "string"
429
+ }
430
+ },
431
+ style: "form",
432
+ explode: true
433
+ }
434
+ ],
435
+ responses: {
436
+ "200": {
437
+ description: "List of categories",
438
+ content: {
439
+ "application/json": {
440
+ schema: {
441
+ type: "array",
442
+ items: {
443
+ type: "string"
444
+ }
445
+ }
446
+ }
447
+ }
448
+ },
449
+ "400": {
450
+ description: "Bad request - invalid entityRef or missing annotation",
451
+ content: {
452
+ "application/json": {
453
+ schema: {
454
+ $ref: "#/components/schemas/RequestError"
455
+ }
456
+ }
457
+ }
458
+ },
459
+ "500": {
460
+ description: "Internal server error",
461
+ content: {
462
+ "application/json": {
463
+ schema: {
464
+ $ref: "#/components/schemas/RequestError"
465
+ }
466
+ }
467
+ }
468
+ },
469
+ "502": {
470
+ description: "Bad Gateway",
471
+ content: {
472
+ "application/json": {
473
+ schema: {
474
+ $ref: "#/components/schemas/RequestError"
475
+ }
476
+ }
477
+ }
478
+ },
479
+ "503": {
480
+ description: "Service Unavailable",
481
+ content: {
482
+ "application/json": {
483
+ schema: {
484
+ $ref: "#/components/schemas/RequestError"
485
+ }
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+ },
218
492
  "/v1/namespaces": {
219
493
  get: {
220
494
  summary: "Get namespaces",
@@ -304,6 +578,139 @@ const spec = {
304
578
  }
305
579
  }
306
580
  }
581
+ },
582
+ "502": {
583
+ description: "Bad Gateway",
584
+ content: {
585
+ "application/json": {
586
+ schema: {
587
+ $ref: "#/components/schemas/RequestError"
588
+ }
589
+ }
590
+ }
591
+ },
592
+ "503": {
593
+ description: "Service Unavailable",
594
+ content: {
595
+ "application/json": {
596
+ schema: {
597
+ $ref: "#/components/schemas/RequestError"
598
+ }
599
+ }
600
+ }
601
+ }
602
+ }
603
+ }
604
+ },
605
+ "/v1/namespaced-resources/policies": {
606
+ get: {
607
+ summary: "Get policies with namespaced policy results",
608
+ description: "List of all Policies with namespace scoped results",
609
+ operationId: "getPolicies",
610
+ parameters: [
611
+ {
612
+ name: "environment",
613
+ in: "query",
614
+ required: true,
615
+ description: "The environment entity reference (URL encoded)",
616
+ schema: {
617
+ type: "string",
618
+ example: "default/component/my-service"
619
+ }
620
+ },
621
+ {
622
+ name: "sources",
623
+ in: "query",
624
+ description: "Filter by a list of sources",
625
+ schema: {
626
+ type: "array",
627
+ items: {
628
+ type: "string"
629
+ }
630
+ },
631
+ style: "form",
632
+ explode: true
633
+ },
634
+ {
635
+ name: "namespaces",
636
+ in: "query",
637
+ description: "Filter by a list of namespaces",
638
+ schema: {
639
+ type: "array",
640
+ items: {
641
+ type: "string"
642
+ }
643
+ },
644
+ style: "form",
645
+ explode: true
646
+ },
647
+ {
648
+ name: "categories",
649
+ in: "query",
650
+ description: "Filter by a list of categories",
651
+ schema: {
652
+ type: "array",
653
+ items: {
654
+ type: "string"
655
+ }
656
+ },
657
+ style: "form",
658
+ explode: true
659
+ }
660
+ ],
661
+ responses: {
662
+ "200": {
663
+ description: "List of policies",
664
+ content: {
665
+ "application/json": {
666
+ schema: {
667
+ type: "array",
668
+ items: {
669
+ type: "string"
670
+ }
671
+ }
672
+ }
673
+ }
674
+ },
675
+ "400": {
676
+ description: "Bad request - invalid entityRef or missing annotation",
677
+ content: {
678
+ "application/json": {
679
+ schema: {
680
+ $ref: "#/components/schemas/RequestError"
681
+ }
682
+ }
683
+ }
684
+ },
685
+ "500": {
686
+ description: "Internal server error - failed to fetch policies",
687
+ content: {
688
+ "application/json": {
689
+ schema: {
690
+ $ref: "#/components/schemas/RequestError"
691
+ }
692
+ }
693
+ }
694
+ },
695
+ "502": {
696
+ description: "Bad Gateway",
697
+ content: {
698
+ "application/json": {
699
+ schema: {
700
+ $ref: "#/components/schemas/RequestError"
701
+ }
702
+ }
703
+ }
704
+ },
705
+ "503": {
706
+ description: "Service Unavailable",
707
+ content: {
708
+ "application/json": {
709
+ schema: {
710
+ $ref: "#/components/schemas/RequestError"
711
+ }
712
+ }
713
+ }
307
714
  }
308
715
  }
309
716
  }