json-schema-diff 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +215 -0
- data/README.md +485 -0
- data/Rakefile +8 -0
- data/SECURITY.md +143 -0
- data/examples/capslock/README.md +27 -0
- data/examples/capslock/capslock-v0.5.0.json +78 -0
- data/examples/capslock/capslock-v0.6.0.json +113 -0
- data/examples/capslock/capslock.schema.json +169 -0
- data/examples/generic/report-v1.2.0.json +63 -0
- data/examples/generic/report-v1.3.0.json +77 -0
- data/examples/generic/security-report.schema.json +149 -0
- data/examples/zizmor/README.md +26 -0
- data/examples/zizmor/zizmor-v0.1.0.json +108 -0
- data/examples/zizmor/zizmor-v0.2.0.json +160 -0
- data/examples/zizmor/zizmor.schema.json +300 -0
- data/exe/json-schema-diff +6 -0
- data/lib/json/schema/diff/cli.rb +101 -0
- data/lib/json/schema/diff/comparer.rb +83 -0
- data/lib/json/schema/diff/formatter.rb +149 -0
- data/lib/json/schema/diff/schema_parser.rb +71 -0
- data/lib/json/schema/diff/version.rb +9 -0
- data/lib/json/schema/diff.rb +17 -0
- data/sig/json/schema/diff.rbs +8 -0
- metadata +70 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"ident": "template-injection",
|
|
4
|
+
"desc": "Template injection in workflow",
|
|
5
|
+
"url": "https://docs.zizmor.sh/audits/template-injection/",
|
|
6
|
+
"determinations": {
|
|
7
|
+
"confidence": "High",
|
|
8
|
+
"severity": "High",
|
|
9
|
+
"persona": "Regular"
|
|
10
|
+
},
|
|
11
|
+
"locations": [
|
|
12
|
+
{
|
|
13
|
+
"symbolic": {
|
|
14
|
+
"key": {
|
|
15
|
+
"Local": {
|
|
16
|
+
"prefix": null,
|
|
17
|
+
"given_path": ".github/workflows/ci.yml"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"annotation": "workflow step uses uncontrolled template expression",
|
|
21
|
+
"route": {
|
|
22
|
+
"route": [
|
|
23
|
+
{"Key": "jobs"},
|
|
24
|
+
{"Key": "test"},
|
|
25
|
+
{"Key": "steps"},
|
|
26
|
+
{"Index": 2},
|
|
27
|
+
{"Key": "run"}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"feature_kind": "Normal",
|
|
31
|
+
"kind": "Primary"
|
|
32
|
+
},
|
|
33
|
+
"concrete": {
|
|
34
|
+
"location": {
|
|
35
|
+
"start_point": {
|
|
36
|
+
"row": 42,
|
|
37
|
+
"column": 12
|
|
38
|
+
},
|
|
39
|
+
"end_point": {
|
|
40
|
+
"row": 42,
|
|
41
|
+
"column": 45
|
|
42
|
+
},
|
|
43
|
+
"offset_span": {
|
|
44
|
+
"start": 1250,
|
|
45
|
+
"end": 1283
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"feature": "echo \"Hello ${{ github.event.issue.title }}\"",
|
|
49
|
+
"comments": []
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"ignored": false
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"ident": "dangerous-actions",
|
|
57
|
+
"desc": "Use of dangerous or deprecated action",
|
|
58
|
+
"url": "https://docs.zizmor.sh/audits/dangerous-actions/",
|
|
59
|
+
"determinations": {
|
|
60
|
+
"confidence": "Medium",
|
|
61
|
+
"severity": "Medium",
|
|
62
|
+
"persona": "Regular"
|
|
63
|
+
},
|
|
64
|
+
"locations": [
|
|
65
|
+
{
|
|
66
|
+
"symbolic": {
|
|
67
|
+
"key": {
|
|
68
|
+
"Local": {
|
|
69
|
+
"prefix": null,
|
|
70
|
+
"given_path": ".github/workflows/deploy.yml"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"annotation": "potentially dangerous action with write permissions",
|
|
74
|
+
"route": {
|
|
75
|
+
"route": [
|
|
76
|
+
{"Key": "jobs"},
|
|
77
|
+
{"Key": "deploy"},
|
|
78
|
+
{"Key": "steps"},
|
|
79
|
+
{"Index": 0},
|
|
80
|
+
{"Key": "uses"}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"feature_kind": "Normal",
|
|
84
|
+
"kind": "Primary"
|
|
85
|
+
},
|
|
86
|
+
"concrete": {
|
|
87
|
+
"location": {
|
|
88
|
+
"start_point": {
|
|
89
|
+
"row": 25,
|
|
90
|
+
"column": 8
|
|
91
|
+
},
|
|
92
|
+
"end_point": {
|
|
93
|
+
"row": 25,
|
|
94
|
+
"column": 35
|
|
95
|
+
},
|
|
96
|
+
"offset_span": {
|
|
97
|
+
"start": 780,
|
|
98
|
+
"end": 807
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"feature": "uses: suspicious/action@v1",
|
|
102
|
+
"comments": []
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"ignored": false
|
|
107
|
+
}
|
|
108
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"ident": "template-injection",
|
|
4
|
+
"desc": "Template injection in workflow",
|
|
5
|
+
"url": "https://docs.zizmor.sh/audits/template-injection/",
|
|
6
|
+
"determinations": {
|
|
7
|
+
"confidence": "High",
|
|
8
|
+
"severity": "High",
|
|
9
|
+
"persona": "Regular"
|
|
10
|
+
},
|
|
11
|
+
"locations": [
|
|
12
|
+
{
|
|
13
|
+
"symbolic": {
|
|
14
|
+
"key": {
|
|
15
|
+
"Local": {
|
|
16
|
+
"prefix": null,
|
|
17
|
+
"given_path": ".github/workflows/ci.yml"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"annotation": "workflow step uses uncontrolled template expression",
|
|
21
|
+
"route": {
|
|
22
|
+
"route": [
|
|
23
|
+
{"Key": "jobs"},
|
|
24
|
+
{"Key": "test"},
|
|
25
|
+
{"Key": "steps"},
|
|
26
|
+
{"Index": 2},
|
|
27
|
+
{"Key": "run"}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"feature_kind": "Normal",
|
|
31
|
+
"kind": "Primary"
|
|
32
|
+
},
|
|
33
|
+
"concrete": {
|
|
34
|
+
"location": {
|
|
35
|
+
"start_point": {
|
|
36
|
+
"row": 42,
|
|
37
|
+
"column": 12
|
|
38
|
+
},
|
|
39
|
+
"end_point": {
|
|
40
|
+
"row": 42,
|
|
41
|
+
"column": 45
|
|
42
|
+
},
|
|
43
|
+
"offset_span": {
|
|
44
|
+
"start": 1250,
|
|
45
|
+
"end": 1283
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"feature": "echo \"Hello ${{ github.event.issue.title }}\"",
|
|
49
|
+
"comments": []
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"ignored": false
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"ident": "dangerous-actions",
|
|
57
|
+
"desc": "Use of dangerous or deprecated action",
|
|
58
|
+
"url": "https://docs.zizmor.sh/audits/dangerous-actions/",
|
|
59
|
+
"determinations": {
|
|
60
|
+
"confidence": "High",
|
|
61
|
+
"severity": "High",
|
|
62
|
+
"persona": "Regular"
|
|
63
|
+
},
|
|
64
|
+
"locations": [
|
|
65
|
+
{
|
|
66
|
+
"symbolic": {
|
|
67
|
+
"key": {
|
|
68
|
+
"Local": {
|
|
69
|
+
"prefix": null,
|
|
70
|
+
"given_path": ".github/workflows/deploy.yml"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"annotation": "potentially dangerous action with write permissions",
|
|
74
|
+
"route": {
|
|
75
|
+
"route": [
|
|
76
|
+
{"Key": "jobs"},
|
|
77
|
+
{"Key": "deploy"},
|
|
78
|
+
{"Key": "steps"},
|
|
79
|
+
{"Index": 0},
|
|
80
|
+
{"Key": "uses"}
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"feature_kind": "Normal",
|
|
84
|
+
"kind": "Primary"
|
|
85
|
+
},
|
|
86
|
+
"concrete": {
|
|
87
|
+
"location": {
|
|
88
|
+
"start_point": {
|
|
89
|
+
"row": 25,
|
|
90
|
+
"column": 8
|
|
91
|
+
},
|
|
92
|
+
"end_point": {
|
|
93
|
+
"row": 25,
|
|
94
|
+
"column": 35
|
|
95
|
+
},
|
|
96
|
+
"offset_span": {
|
|
97
|
+
"start": 780,
|
|
98
|
+
"end": 807
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"feature": "uses: suspicious/action@v1",
|
|
102
|
+
"comments": []
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"ignored": false
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"ident": "hardcoded-credentials",
|
|
110
|
+
"desc": "Hardcoded credentials detected",
|
|
111
|
+
"url": "https://docs.zizmor.sh/audits/hardcoded-credentials/",
|
|
112
|
+
"determinations": {
|
|
113
|
+
"confidence": "High",
|
|
114
|
+
"severity": "High",
|
|
115
|
+
"persona": "Regular"
|
|
116
|
+
},
|
|
117
|
+
"locations": [
|
|
118
|
+
{
|
|
119
|
+
"symbolic": {
|
|
120
|
+
"key": {
|
|
121
|
+
"Local": {
|
|
122
|
+
"prefix": null,
|
|
123
|
+
"given_path": ".github/workflows/release.yml"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"annotation": "hardcoded API key in environment variable",
|
|
127
|
+
"route": {
|
|
128
|
+
"route": [
|
|
129
|
+
{"Key": "jobs"},
|
|
130
|
+
{"Key": "publish"},
|
|
131
|
+
{"Key": "env"},
|
|
132
|
+
{"Key": "API_KEY"}
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"feature_kind": "Normal",
|
|
136
|
+
"kind": "Primary"
|
|
137
|
+
},
|
|
138
|
+
"concrete": {
|
|
139
|
+
"location": {
|
|
140
|
+
"start_point": {
|
|
141
|
+
"row": 18,
|
|
142
|
+
"column": 4
|
|
143
|
+
},
|
|
144
|
+
"end_point": {
|
|
145
|
+
"row": 18,
|
|
146
|
+
"column": 35
|
|
147
|
+
},
|
|
148
|
+
"offset_span": {
|
|
149
|
+
"start": 560,
|
|
150
|
+
"end": 591
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"feature": "API_KEY: sk-1234567890abcdef",
|
|
154
|
+
"comments": []
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"ignored": false
|
|
159
|
+
}
|
|
160
|
+
]
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://docs.zizmor.sh/schemas/output-v1.json",
|
|
4
|
+
"title": "Zizmor JSON Output Schema (v1)",
|
|
5
|
+
"description": "Schema for zizmor's v1 JSON output format - an array of security findings",
|
|
6
|
+
"type": "array",
|
|
7
|
+
"items": {
|
|
8
|
+
"$ref": "#/$defs/Finding"
|
|
9
|
+
},
|
|
10
|
+
"$defs": {
|
|
11
|
+
"Finding": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"description": "A security finding identified by zizmor",
|
|
14
|
+
"required": ["ident", "desc", "url", "determinations", "locations", "ignored"],
|
|
15
|
+
"properties": {
|
|
16
|
+
"ident": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "The audit ID for this finding (e.g., 'template-injection')"
|
|
19
|
+
},
|
|
20
|
+
"desc": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "A short description of the finding"
|
|
23
|
+
},
|
|
24
|
+
"url": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"format": "uri",
|
|
27
|
+
"description": "A URL linking to the documentation for this finding's audit"
|
|
28
|
+
},
|
|
29
|
+
"determinations": {
|
|
30
|
+
"$ref": "#/$defs/Determinations"
|
|
31
|
+
},
|
|
32
|
+
"locations": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"description": "Array of locations where this finding occurs",
|
|
35
|
+
"items": {
|
|
36
|
+
"$ref": "#/$defs/Location"
|
|
37
|
+
},
|
|
38
|
+
"minItems": 1
|
|
39
|
+
},
|
|
40
|
+
"ignored": {
|
|
41
|
+
"type": "boolean",
|
|
42
|
+
"description": "Whether this finding is ignored via inline comments or configuration"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"additionalProperties": false
|
|
46
|
+
},
|
|
47
|
+
"Determinations": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"description": "Classifications for the finding's confidence, severity, and target persona",
|
|
50
|
+
"required": ["confidence", "severity", "persona"],
|
|
51
|
+
"properties": {
|
|
52
|
+
"confidence": {
|
|
53
|
+
"$ref": "#/$defs/Confidence"
|
|
54
|
+
},
|
|
55
|
+
"severity": {
|
|
56
|
+
"$ref": "#/$defs/Severity"
|
|
57
|
+
},
|
|
58
|
+
"persona": {
|
|
59
|
+
"$ref": "#/$defs/Persona"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"additionalProperties": false
|
|
63
|
+
},
|
|
64
|
+
"Confidence": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"enum": ["Unknown", "Low", "Medium", "High"],
|
|
67
|
+
"description": "Confidence level of the finding"
|
|
68
|
+
},
|
|
69
|
+
"Severity": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"enum": ["Unknown", "Informational", "Low", "Medium", "High"],
|
|
72
|
+
"description": "Severity level of the finding"
|
|
73
|
+
},
|
|
74
|
+
"Persona": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"enum": ["Auditor", "Pedantic", "Regular"],
|
|
77
|
+
"description": "Target persona for the finding (affects false positive tolerance)"
|
|
78
|
+
},
|
|
79
|
+
"Location": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"description": "A location within a GitHub Actions workflow with symbolic and concrete components",
|
|
82
|
+
"required": ["symbolic", "concrete"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"symbolic": {
|
|
85
|
+
"$ref": "#/$defs/SymbolicLocation"
|
|
86
|
+
},
|
|
87
|
+
"concrete": {
|
|
88
|
+
"$ref": "#/$defs/Feature"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"additionalProperties": false
|
|
92
|
+
},
|
|
93
|
+
"SymbolicLocation": {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"description": "Symbolic representation of a location in the workflow",
|
|
96
|
+
"required": ["key", "annotation", "route", "feature_kind", "kind"],
|
|
97
|
+
"properties": {
|
|
98
|
+
"key": {
|
|
99
|
+
"$ref": "#/$defs/InputKey"
|
|
100
|
+
},
|
|
101
|
+
"annotation": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "Human-readable annotation for this location"
|
|
104
|
+
},
|
|
105
|
+
"route": {
|
|
106
|
+
"$ref": "#/$defs/Route"
|
|
107
|
+
},
|
|
108
|
+
"feature_kind": {
|
|
109
|
+
"$ref": "#/$defs/SymbolicFeature"
|
|
110
|
+
},
|
|
111
|
+
"kind": {
|
|
112
|
+
"$ref": "#/$defs/LocationKind"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"additionalProperties": false
|
|
116
|
+
},
|
|
117
|
+
"InputKey": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"description": "Identifier for the input file",
|
|
120
|
+
"oneOf": [
|
|
121
|
+
{
|
|
122
|
+
"type": "object",
|
|
123
|
+
"required": ["Local"],
|
|
124
|
+
"properties": {
|
|
125
|
+
"Local": {
|
|
126
|
+
"type": "object",
|
|
127
|
+
"required": ["prefix", "given_path"],
|
|
128
|
+
"properties": {
|
|
129
|
+
"prefix": {
|
|
130
|
+
"type": ["string", "null"],
|
|
131
|
+
"description": "Optional prefix for the path"
|
|
132
|
+
},
|
|
133
|
+
"given_path": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"description": "The file path"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"additionalProperties": false
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"additionalProperties": false
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
"Route": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"description": "Path through the YAML structure to reach the location",
|
|
148
|
+
"required": ["route"],
|
|
149
|
+
"properties": {
|
|
150
|
+
"route": {
|
|
151
|
+
"type": "array",
|
|
152
|
+
"items": {
|
|
153
|
+
"$ref": "#/$defs/RouteComponent"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"additionalProperties": false
|
|
158
|
+
},
|
|
159
|
+
"RouteComponent": {
|
|
160
|
+
"oneOf": [
|
|
161
|
+
{
|
|
162
|
+
"type": "object",
|
|
163
|
+
"required": ["Key"],
|
|
164
|
+
"properties": {
|
|
165
|
+
"Key": {
|
|
166
|
+
"type": "string"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"additionalProperties": false
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"type": "object",
|
|
173
|
+
"required": ["Index"],
|
|
174
|
+
"properties": {
|
|
175
|
+
"Index": {
|
|
176
|
+
"type": "integer",
|
|
177
|
+
"minimum": 0
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"additionalProperties": false
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
"SymbolicFeature": {
|
|
185
|
+
"oneOf": [
|
|
186
|
+
{
|
|
187
|
+
"type": "string",
|
|
188
|
+
"enum": ["Normal", "KeyOnly"],
|
|
189
|
+
"description": "Normal feature (whole YAML feature) or KeyOnly (key without value)"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"type": "object",
|
|
193
|
+
"required": ["Subfeature"],
|
|
194
|
+
"properties": {
|
|
195
|
+
"Subfeature": {
|
|
196
|
+
"type": "object",
|
|
197
|
+
"required": ["after", "fragment"],
|
|
198
|
+
"properties": {
|
|
199
|
+
"after": {
|
|
200
|
+
"type": "integer",
|
|
201
|
+
"minimum": 0,
|
|
202
|
+
"description": "Byte offset within the parent feature"
|
|
203
|
+
},
|
|
204
|
+
"fragment": {
|
|
205
|
+
"type": "object",
|
|
206
|
+
"required": ["Raw"],
|
|
207
|
+
"properties": {
|
|
208
|
+
"Raw": {
|
|
209
|
+
"type": "string",
|
|
210
|
+
"description": "The raw text fragment"
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"additionalProperties": false
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"additionalProperties": false
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"additionalProperties": false
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
"LocationKind": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"enum": ["Primary", "Related", "Hidden"],
|
|
226
|
+
"description": "Type of location: Primary (main issue), Related (supporting), or Hidden (not displayed)"
|
|
227
|
+
},
|
|
228
|
+
"Feature": {
|
|
229
|
+
"type": "object",
|
|
230
|
+
"description": "Concrete extracted feature with location and content",
|
|
231
|
+
"required": ["location", "feature", "comments"],
|
|
232
|
+
"properties": {
|
|
233
|
+
"location": {
|
|
234
|
+
"$ref": "#/$defs/ConcreteLocation"
|
|
235
|
+
},
|
|
236
|
+
"feature": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": "The extracted textual content of the feature"
|
|
239
|
+
},
|
|
240
|
+
"comments": {
|
|
241
|
+
"type": "array",
|
|
242
|
+
"items": {
|
|
243
|
+
"type": "string"
|
|
244
|
+
},
|
|
245
|
+
"description": "Any comments within the feature's line span"
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
"additionalProperties": false
|
|
249
|
+
},
|
|
250
|
+
"ConcreteLocation": {
|
|
251
|
+
"type": "object",
|
|
252
|
+
"description": "Concrete location with line/column and byte offset information",
|
|
253
|
+
"required": ["start_point", "end_point", "offset_span"],
|
|
254
|
+
"properties": {
|
|
255
|
+
"start_point": {
|
|
256
|
+
"$ref": "#/$defs/Point"
|
|
257
|
+
},
|
|
258
|
+
"end_point": {
|
|
259
|
+
"$ref": "#/$defs/Point"
|
|
260
|
+
},
|
|
261
|
+
"offset_span": {
|
|
262
|
+
"type": "object",
|
|
263
|
+
"required": ["start", "end"],
|
|
264
|
+
"properties": {
|
|
265
|
+
"start": {
|
|
266
|
+
"type": "integer",
|
|
267
|
+
"minimum": 0,
|
|
268
|
+
"description": "Starting byte offset"
|
|
269
|
+
},
|
|
270
|
+
"end": {
|
|
271
|
+
"type": "integer",
|
|
272
|
+
"minimum": 0,
|
|
273
|
+
"description": "Ending byte offset"
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
"additionalProperties": false
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"additionalProperties": false
|
|
280
|
+
},
|
|
281
|
+
"Point": {
|
|
282
|
+
"type": "object",
|
|
283
|
+
"description": "A (row, column) point within a file",
|
|
284
|
+
"required": ["row", "column"],
|
|
285
|
+
"properties": {
|
|
286
|
+
"row": {
|
|
287
|
+
"type": "integer",
|
|
288
|
+
"minimum": 0,
|
|
289
|
+
"description": "Zero-indexed row number"
|
|
290
|
+
},
|
|
291
|
+
"column": {
|
|
292
|
+
"type": "integer",
|
|
293
|
+
"minimum": 0,
|
|
294
|
+
"description": "Zero-indexed column number"
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
"additionalProperties": false
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Json
|
|
4
|
+
module Schema
|
|
5
|
+
module Diff
|
|
6
|
+
class CLI
|
|
7
|
+
def self.start(args)
|
|
8
|
+
new.run(args)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run(args)
|
|
12
|
+
options = parse_options(args)
|
|
13
|
+
|
|
14
|
+
if args.length != 3
|
|
15
|
+
puts "Usage: json-schema-diff [OPTIONS] SCHEMA OLD_JSON NEW_JSON"
|
|
16
|
+
puts "Try 'json-schema-diff --help' for more information."
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
schema_file, old_file, new_file = args
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
schema = SchemaParser.new(schema_file)
|
|
24
|
+
comparer = Comparer.new(schema, options[:ignore_fields] || [])
|
|
25
|
+
|
|
26
|
+
old_json = JSON.parse(File.read(old_file))
|
|
27
|
+
new_json = JSON.parse(File.read(new_file))
|
|
28
|
+
|
|
29
|
+
diff_result = comparer.compare(old_json, new_json)
|
|
30
|
+
|
|
31
|
+
formatter = Formatter.new(options[:format], options[:color])
|
|
32
|
+
puts formatter.format(diff_result)
|
|
33
|
+
rescue JSON::ParserError => e
|
|
34
|
+
puts "Error parsing JSON: #{e.message}"
|
|
35
|
+
exit 1
|
|
36
|
+
rescue Errno::ENOENT => e
|
|
37
|
+
puts "File not found: #{e.message}"
|
|
38
|
+
exit 1
|
|
39
|
+
rescue Error => e
|
|
40
|
+
puts "Error: #{e.message}"
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def parse_options(args)
|
|
48
|
+
options = {
|
|
49
|
+
format: "pretty",
|
|
50
|
+
color: true,
|
|
51
|
+
ignore_fields: []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
parser = OptionParser.new do |opts|
|
|
55
|
+
opts.banner = "Usage: json-schema-diff [OPTIONS] SCHEMA OLD_JSON NEW_JSON"
|
|
56
|
+
opts.separator ""
|
|
57
|
+
opts.separator "Compare two JSON files using a JSON Schema to guide and annotate the diff output."
|
|
58
|
+
opts.separator ""
|
|
59
|
+
opts.separator "Arguments:"
|
|
60
|
+
opts.separator " SCHEMA Path to JSON Schema file"
|
|
61
|
+
opts.separator " OLD_JSON Path to first JSON file (baseline)"
|
|
62
|
+
opts.separator " NEW_JSON Path to second JSON file (comparison)"
|
|
63
|
+
opts.separator ""
|
|
64
|
+
opts.separator "Options:"
|
|
65
|
+
|
|
66
|
+
opts.on("-f", "--format FORMAT", ["pretty", "json"],
|
|
67
|
+
"Output format (pretty, json)") do |format|
|
|
68
|
+
options[:format] = format
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
opts.on("-i", "--ignore-fields FIELDS", Array,
|
|
72
|
+
"Comma-separated list of field paths to ignore") do |fields|
|
|
73
|
+
options[:ignore_fields] = fields
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
opts.on("--[no-]color", "Enable/disable colored output (default: enabled)") do |color|
|
|
77
|
+
options[:color] = color
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
81
|
+
puts opts
|
|
82
|
+
exit 0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
opts.on("-v", "--version", "Show version") do
|
|
86
|
+
puts "json-schema-diff #{VERSION}"
|
|
87
|
+
exit 0
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
parser.parse!(args)
|
|
92
|
+
options
|
|
93
|
+
rescue OptionParser::InvalidOption => e
|
|
94
|
+
puts "Error: #{e.message}"
|
|
95
|
+
puts "Try 'json-schema-diff --help' for more information."
|
|
96
|
+
exit 1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|