@@ -175,7 +175,108 @@ func (s APIServer) GetRuleByRuleId(w http.ResponseWriter, r *http.Request, ruleI
175175 writeAuthError (w , http .StatusInternalServerError , "failed to get rule" )
176176 return
177177 }
178- writeJSON (w , http .StatusOK , ruleToAPI (* row ))
178+ // The /stigs/rules/{ruleId} lookup card always wants the full set.
179+ writeJSON (w , http .StatusOK , ruleToAPIWith (* row , ruleProjFull ))
180+ }
181+
182+ // ruleProjSet is a parsed view of the ?projection query.
183+ type ruleProjSet struct {
184+ Detail bool
185+ Check bool
186+ Fix bool
187+ CCIs bool
188+ Stigs bool
189+ }
190+
191+ // ruleProjFull is the projection requested by the /stigs/rules/{ruleId}
192+ // lookup card (everything except the cross-revision Stigs nav list).
193+ var ruleProjFull = ruleProjSet {Detail : true , Check : true , Fix : true , CCIs : true , Stigs : true }
194+
195+ // parseRuleProjection turns a *RuleProjectionQuery (which is *[]string
196+ // underneath) into a parsed flag set.
197+ func parseRuleProjection (q * api.RuleProjectionQuery ) ruleProjSet {
198+ out := ruleProjSet {}
199+ if q == nil {
200+ return out
201+ }
202+ for _ , p := range * q {
203+ switch p {
204+ case "detail" :
205+ out .Detail = true
206+ case "check" :
207+ out .Check = true
208+ case "fix" :
209+ out .Fix = true
210+ case "ccis" :
211+ out .CCIs = true
212+ case "stigs" :
213+ out .Stigs = true
214+ }
215+ }
216+ return out
217+ }
218+
219+ // GetRulesByRevision returns all rules for a given (benchmarkId,
220+ // revisionStr) pair. revisionStr may be "latest" to select the most
221+ // recently imported revision.
222+ func (s APIServer ) GetRulesByRevision (w http.ResponseWriter , r * http.Request , benchmarkId string , revisionStr string , params api.GetRulesByRevisionParams ) {
223+ if ! s .requiredScope (w , r , "stig-manager:stig:read" ) {
224+ return
225+ }
226+ if s .Stigs == nil {
227+ writeAuthError (w , http .StatusNotFound , "revision not found" )
228+ return
229+ }
230+ proj := parseRuleProjection (params .Projection )
231+ rows , _ , err := s .Stigs .ListRulesByRevision (r .Context (), benchmarkId , revisionStr , store.RulesByRevisionOptions {
232+ IncludeCCIs : proj .CCIs ,
233+ IncludeDetail : proj .Detail ,
234+ IncludeCheck : proj .Check ,
235+ IncludeFix : proj .Fix ,
236+ })
237+ if err != nil {
238+ if errors .Is (err , store .ErrNotFound ) {
239+ writeAuthError (w , http .StatusNotFound , "revision not found" )
240+ return
241+ }
242+ s .logErr (r , "list rules by revision" , err )
243+ writeAuthError (w , http .StatusInternalServerError , "failed to list rules" )
244+ return
245+ }
246+ out := make ([]api.RuleProjected , 0 , len (rows ))
247+ for _ , row := range rows {
248+ out = append (out , ruleToAPIWith (row , proj ))
249+ }
250+ writeJSON (w , http .StatusOK , out )
251+ }
252+
253+ // GetRuleByRevision returns a single rule projection scoped to a
254+ // specific (benchmarkId, revisionStr).
255+ func (s APIServer ) GetRuleByRevision (w http.ResponseWriter , r * http.Request , benchmarkId string , revisionStr string , ruleId string , params api.GetRuleByRevisionParams ) {
256+ if ! s .requiredScope (w , r , "stig-manager:stig:read" ) {
257+ return
258+ }
259+ if s .Stigs == nil {
260+ writeAuthError (w , http .StatusNotFound , "rule not found" )
261+ return
262+ }
263+ proj := parseRuleProjection (params .Projection )
264+ row , err := s .Stigs .GetRuleByRevision (r .Context (), benchmarkId , revisionStr , ruleId , store.RulesByRevisionOptions {
265+ IncludeCCIs : proj .CCIs ,
266+ IncludeDetail : proj .Detail ,
267+ IncludeCheck : proj .Check ,
268+ IncludeFix : proj .Fix ,
269+ })
270+ if err != nil {
271+ if errors .Is (err , store .ErrNotFound ) {
272+ writeAuthError (w , http .StatusNotFound , "rule not found" )
273+ return
274+ }
275+ s .logErr (r , "get rule by revision" , err )
276+ writeAuthError (w , http .StatusInternalServerError , "failed to get rule" )
277+ return
278+ }
279+ writeJSON (w , http .StatusOK , ruleToAPIWith (* row , proj ))
179280}
180281
181282// GetCci returns a single CCI projection plus the STIGs that reference
@@ -233,14 +334,15 @@ func storeToAPIStig(row store.STIG, withRevisions bool) api.STIG {
233334 return out
234335}
235336
236- // ruleToAPI projects a store.RuleProjection into the OpenAPI
237- // RuleProjected schema returned by GET /stigs/rules/{ruleId}.
238- func ruleToAPI (row store.RuleProjection ) api.RuleProjected {
337+ // ruleToAPIWith projects a store.RuleProjection into the OpenAPI
338+ // RuleProjected schema, only emitting the heavy fields the caller
339+ // asked for via ?projection.
340+ func ruleToAPIWith (row store.RuleProjection , proj ruleProjSet ) api.RuleProjected {
239341 rid := api .RuleId (row .RuleID )
240342 title := api .RuleTitle (row .Title )
241343 version := api .VersionString (row .VersionStr )
242- groupID := api .GroupId (row .RevisionStr )
243- groupTitle := api .GroupTitle ("" )
344+ groupID := api .GroupId (row .GroupID )
345+ groupTitle := api .GroupTitle (row . GroupTitle )
244346
245347 out := api.RuleProjected {
246348 RuleId : & rid ,
@@ -251,26 +353,26 @@ func ruleToAPI(row store.RuleProjection) api.RuleProjected {
251353 GroupTitle : & groupTitle ,
252354 }
253355
254- if row .CheckContent != "" || row .CheckSystem != "" {
356+ if proj . Check && ( row .CheckContent != "" || row .CheckSystem != "" ) {
255357 out .Check = & api.Check {
256358 Content : strPtr (row .CheckContent ),
257359 System : strPtr (row .CheckSystem ),
258360 }
259361 }
260- if row .FixText != "" || row .FixID != "" {
362+ if proj . Fix && ( row .FixText != "" || row .FixID != "" ) {
261363 out .Fix = & api.Fix {
262364 Text : strPtr (row .FixText ),
263365 Fixref : strPtr (row .FixID ),
264366 }
265367 }
266- if len (row .CCIs ) > 0 {
368+ if proj . CCIs && len (row .CCIs ) > 0 {
267369 basics := make ([]api.CciBasic , 0 , len (row .CCIs ))
268370 for _ , cci := range row .CCIs {
269371 basics = append (basics , api.CciBasic {Cci : api .CciString (cci )})
270372 }
271373 out .Ccis = & basics
272374 }
273- if row .Description != "" {
375+ if proj . Detail && row .Description != "" {
274376 // Embed the raw VulnDiscussion blob under detail.vulnDiscussion;
275377 // fine-grained parsing of the DISA pseudo-XML lives behind the
276378 // review-content milestone.
@@ -289,7 +391,7 @@ func ruleToAPI(row store.RuleProjection) api.RuleProjected {
289391 Weight * string `json:"weight,omitempty"`
290392 }{VulnDiscussion : & desc }
291393 }
292- if row .BenchmarkID != "" {
394+ if proj . Stigs && row .BenchmarkID != "" {
293395 bid := api .BenchmarkId (row .BenchmarkID )
294396 out .Stigs = & []api.RevisionBasic {{
295397 BenchmarkId : & bid ,
0 commit comments