dn-scribe commited on
Commit
779f6a4
·
2 Parent(s): 1b5aa1d09fb66a

Merge branch 'main' into 42-badges-fixes

Browse files
CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
 
3
  <!-- version list -->
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  ## v1.0.5 (2025-09-29)
6
 
7
  ### Bug Fixes
 
2
 
3
  <!-- version list -->
4
 
5
+ ## v1.1.1 (2025-10-05)
6
+
7
+ ### Bug Fixes
8
+
9
+ - Add content_type to attestation table
10
+ ([`2fd68a5`](https://github.com/scribe-security/attstore/commit/2fd68a52b8284049026d4a64545675343a3b8116))
11
+
12
+ - Lock colors
13
+ ([`ee842bb`](https://github.com/scribe-security/attstore/commit/ee842bbb817fba4747d18fa334f5947e6097dc30))
14
+
15
+
16
+ ## v1.1.0 (2025-10-05)
17
+
18
+ ### Features
19
+
20
+ - Filter products by version substrings
21
+ ([`aa9e4d9`](https://github.com/scribe-security/attstore/commit/aa9e4d95741fc70f1801d517af9e8a72637a6538))
22
+
23
+ - Support filtering attestations and policy results by version.
24
+ ([`735e852`](https://github.com/scribe-security/attstore/commit/735e85285c74372db35d6e0237b15ae3e0b83ede))
25
+
26
+
27
  ## v1.0.5 (2025-09-29)
28
 
29
  ### Bug Fixes
static/css/custom.css CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  /* Custom CSS for Attestation Store */
2
 
3
  /* Root variables for consistency */
 
1
+ /* Custom lock icon colors for attestation page */
2
+ .icon-lock-locked {
3
+ color: #4fc3f7 !important; /* Light blue */
4
+ }
5
+ .icon-lock-unlocked {
6
+ color: #ba68c8 !important; /* Purple */
7
+ }
8
  /* Custom CSS for Attestation Store */
9
 
10
  /* Root variables for consistency */
templates/attestations.html CHANGED
@@ -32,6 +32,9 @@
32
  {% endfor %}
33
  </select>
34
  </div>
 
 
 
35
  <div class="col-md-3">
36
  <select class="form-select" name="target_type" aria-label="Filter by target type">
37
  <option value="">Target Type (all)</option>
@@ -123,11 +126,14 @@
123
  <td>
124
  <div class="d-inline-flex align-items-center">
125
  {% if attestation.sig_status == 'signed' %}
126
- <i data-feather="lock" class="{% if attestation.deleted %}text-secondary{% else %}text-success{% endif %} me-2" style="width: 16px; height: 16px;" title="Signed attestation"></i>
127
  {% else %}
128
- <i data-feather="unlock" class="{% if attestation.deleted %}text-secondary{% else %}text-warning{% endif %} me-2" style="width: 16px; height: 16px;" title="Unsigned attestation"></i>
129
  {% endif %}
130
  {{ attestation.target_type }}
 
 
 
131
  </div>
132
  </td>
133
  <td>{{ attestation.tool or 'Unknown' }}</td>
 
32
  {% endfor %}
33
  </select>
34
  </div>
35
+ <div class="col-md-2">
36
+ <input type="text" class="form-control" name="version" placeholder="Version (substring)" value="{{ version_filter }}">
37
+ </div>
38
  <div class="col-md-3">
39
  <select class="form-select" name="target_type" aria-label="Filter by target type">
40
  <option value="">Target Type (all)</option>
 
126
  <td>
127
  <div class="d-inline-flex align-items-center">
128
  {% if attestation.sig_status == 'signed' %}
129
+ <i data-feather="lock" class="{% if attestation.deleted %}text-secondary{% else %}icon-lock-locked{% endif %} me-2" style="width: 16px; height: 16px;" title="Signed attestation"></i>
130
  {% else %}
131
+ <i data-feather="unlock" class="{% if attestation.deleted %}text-secondary{% else %}icon-lock-unlocked{% endif %} me-2" style="width: 16px; height: 16px;" title="Unsigned attestation"></i>
132
  {% endif %}
133
  {{ attestation.target_type }}
134
+ {% if attestation.content_type %}
135
+ <span class="badge bg-secondary ms-2">{{ attestation.content_type }}</span>
136
+ {% endif %}
137
  </div>
138
  </td>
139
  <td>{{ attestation.tool or 'Unknown' }}</td>
templates/policy_results.html CHANGED
@@ -70,6 +70,9 @@
70
  {% endfor %}
71
  </select>
72
  </div>
 
 
 
73
  <div class="col-md-3">
74
  <input type="text" class="form-control" name="rule_id" placeholder="Rule ID (substring)" value="{{ rule_id_filter }}">
75
  </div>
@@ -194,14 +197,15 @@
194
  <!-- Pagination -->
195
  {% if pagination.pages > 1 %}
196
  {% macro page_url(page_num) -%}
197
- {{ url_for('web.policy_results') }}?
198
- {%- if product_filter %}product={{ product_filter|urlencode }}&{% endif -%}
199
- {%- if rule_id_filter %}rule_id={{ rule_id_filter|urlencode }}&{% endif -%}
200
- {%- if result_kind_filter %}result_kind={{ result_kind_filter|urlencode }}&{% endif -%}
201
- {%- if result_level_filter %}result_level={{ result_level_filter|urlencode }}&{% endif -%}
202
- {%- if show_all_users and is_admin %}show_all_users=on&{% endif -%}
203
- {%- if user_filter and show_all_users and is_admin %}user_filter={{ user_filter }}&{% endif -%}
204
- page={{ page_num }}
 
205
  {%- endmacro %}
206
  <nav class="mt-4">
207
  <ul class="pagination justify-content-center">
 
70
  {% endfor %}
71
  </select>
72
  </div>
73
+ <div class="col-md-2">
74
+ <input type="text" class="form-control" name="version" placeholder="Version (substring)" value="{{ version_filter }}">
75
+ </div>
76
  <div class="col-md-3">
77
  <input type="text" class="form-control" name="rule_id" placeholder="Rule ID (substring)" value="{{ rule_id_filter }}">
78
  </div>
 
197
  <!-- Pagination -->
198
  {% if pagination.pages > 1 %}
199
  {% macro page_url(page_num) -%}
200
+ {{ url_for('web.policy_results') }}?
201
+ {%- if product_filter %}product={{ product_filter|urlencode }}&{% endif -%}
202
+ {%- if version_filter %}version={{ version_filter|urlencode }}&{% endif -%}
203
+ {%- if rule_id_filter %}rule_id={{ rule_id_filter|urlencode }}&{% endif -%}
204
+ {%- if result_kind_filter %}result_kind={{ result_kind_filter|urlencode }}&{% endif -%}
205
+ {%- if result_level_filter %}result_level={{ result_level_filter|urlencode }}&{% endif -%}
206
+ {%- if show_all_users and is_admin %}show_all_users=on&{% endif -%}
207
+ {%- if user_filter and show_all_users and is_admin %}user_filter={{ user_filter }}&{% endif -%}
208
+ page={{ page_num }}
209
  {%- endmacro %}
210
  <nav class="mt-4">
211
  <ul class="pagination justify-content-center">
templates/products.html CHANGED
@@ -19,9 +19,12 @@
19
  <div class="card mb-4">
20
  <div class="card-body">
21
  <form method="GET" class="row g-3 align-items-center">
22
- <div class="col-md-4">
23
  <input type="text" class="form-control" name="filter" placeholder="Products substring" value="{{ filter_query }}">
24
  </div>
 
 
 
25
  {% if is_admin %}
26
  <div class="col-md-3 form-check align-items-center d-flex">
27
  <input class="form-check-input me-2" type="checkbox" id="showAllCheckbox" name="show_all" value="1" {% if show_all %}checked{% endif %} onchange="this.form.submit()">
@@ -85,10 +88,10 @@
85
  {% endfor %}
86
  </td>
87
  <td class="text-center">
88
- <a href="/products/{{ p.product }}/attestations" class="btn btn-sm btn-outline-primary me-1">
89
  <i data-feather="file-text" class="me-1"></i> Attestations
90
  </a>
91
- <a href="/products/{{ p.product }}/policy_results" class="btn btn-sm btn-outline-info">
92
  <i data-feather="shield" class="me-1"></i> Policy Results
93
  </a>
94
  </td>
 
19
  <div class="card mb-4">
20
  <div class="card-body">
21
  <form method="GET" class="row g-3 align-items-center">
22
+ <div class="col-md-3">
23
  <input type="text" class="form-control" name="filter" placeholder="Products substring" value="{{ filter_query }}">
24
  </div>
25
+ <div class="col-md-3">
26
+ <input type="text" class="form-control" name="version" placeholder="Version substring" value="{{ version_filter }}">
27
+ </div>
28
  {% if is_admin %}
29
  <div class="col-md-3 form-check align-items-center d-flex">
30
  <input class="form-check-input me-2" type="checkbox" id="showAllCheckbox" name="show_all" value="1" {% if show_all %}checked{% endif %} onchange="this.form.submit()">
 
88
  {% endfor %}
89
  </td>
90
  <td class="text-center">
91
+ <a href="/products/{{ p.product }}/{{ p.version }}/attestations" class="btn btn-sm btn-outline-primary me-1">
92
  <i data-feather="file-text" class="me-1"></i> Attestations
93
  </a>
94
+ <a href="/products/{{ p.product }}/{{ p.version }}/policy_results" class="btn btn-sm btn-outline-info">
95
  <i data-feather="shield" class="me-1"></i> Policy Results
96
  </a>
97
  </td>
version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.0.5"
 
1
+ __version__ = "1.1.1"
web/products.py CHANGED
@@ -2,7 +2,7 @@ from flask import Blueprint, render_template, request, redirect, url_for, sessio
2
  from models import db, Attestation, PolicyResult, Product
3
  from sqlalchemy import func, text
4
 
5
- def get_products_with_stats(filter_query=None, user_id=None, show_all=False):
6
  # Only consume data from products table
7
  query = Product.query.filter(Product.deleted == False)
8
  if not show_all and user_id is not None:
@@ -12,6 +12,9 @@ def get_products_with_stats(filter_query=None, user_id=None, show_all=False):
12
  query = query.filter(
13
  Product.product_key.ilike(like_pattern)
14
  )
 
 
 
15
  # Paging support
16
  page = request.args.get('page', 1, type=int)
17
  per_page = request.args.get('per_page', 20, type=int)
@@ -48,7 +51,6 @@ def get_products_with_stats(filter_query=None, user_id=None, show_all=False):
48
  'user_id': product.user_id
49
  })
50
  return products, pagination
51
- return products
52
 
53
  # Function to update product data
54
  def update_product_data(product_key, product_version, name=None, description=None, metadata=None, badges=None, user_id=None):
 
2
  from models import db, Attestation, PolicyResult, Product
3
  from sqlalchemy import func, text
4
 
5
+ def get_products_with_stats(filter_query=None, version_filter=None, user_id=None, show_all=False):
6
  # Only consume data from products table
7
  query = Product.query.filter(Product.deleted == False)
8
  if not show_all and user_id is not None:
 
12
  query = query.filter(
13
  Product.product_key.ilike(like_pattern)
14
  )
15
+ if version_filter:
16
+ version_like = f'%{version_filter}%'
17
+ query = query.filter(Product.product_version.ilike(version_like))
18
  # Paging support
19
  page = request.args.get('page', 1, type=int)
20
  per_page = request.args.get('per_page', 20, type=int)
 
51
  'user_id': product.user_id
52
  })
53
  return products, pagination
 
54
 
55
  # Function to update product data
56
  def update_product_data(product_key, product_version, name=None, description=None, metadata=None, badges=None, user_id=None):
web/routes.py CHANGED
@@ -224,6 +224,7 @@ def login_required(f):
224
  @login_required
225
  def products_page():
226
  filter_query = request.args.get('filter', '')
 
227
  page = request.args.get('page', 1, type=int)
228
  per_page = request.args.get('per_page', 20, type=int)
229
  user_id = session.get('user_id')
@@ -234,8 +235,8 @@ def products_page():
234
  is_admin = _is_admin_user_web(user_id)
235
  if is_admin:
236
  show_all = request.args.get('show_all', '0') == '1'
237
- products, pagination = get_products_with_stats(filter_query, user_id=user_id, show_all=show_all)
238
- return render_template('products.html', products=products, pagination=pagination, filter_query=filter_query, show_all=show_all, is_admin=is_admin)
239
 
240
  # --- Products CSV Download ---
241
  @web_bp.route('/products/download-csv')
@@ -243,10 +244,11 @@ def products_page():
243
  def download_products_csv():
244
  """Download products table as CSV, one line per product version and badge."""
245
  filter_query = request.args.get('filter', '')
 
246
  page = request.args.get('page', 1, type=int)
247
  per_page = request.args.get('per_page', 20, type=int)
248
  custom_filename = request.args.get('filename', '')
249
- products, pagination = get_products_with_stats(filter_query)
250
  from models import Badge
251
  all_badges = Badge.query.order_by(Badge.name).all()
252
  output = io.StringIO()
@@ -677,6 +679,7 @@ def attestations():
677
 
678
  # Get filter parameters
679
  product_filter = request.args.get('product', '')
 
680
  target_type_filter = request.args.get('target_type', '')
681
  tool_filter = request.args.get('tool', '')
682
  search_filter = request.args.get('search', '')
@@ -693,13 +696,12 @@ def attestations():
693
  # Apply filters
694
  if product_filter and product_filter.strip():
695
  query = query.filter(Attestation.product_key == product_filter.strip())
696
-
 
697
  if target_type_filter:
698
  query = query.filter(Attestation.target_type == target_type_filter)
699
-
700
  if tool_filter:
701
  query = query.filter(Attestation.tool.ilike(f"%{tool_filter}%"))
702
-
703
  if id_filter and id_filter.strip():
704
  try:
705
  attestation_id = int(id_filter.strip())
@@ -707,7 +709,6 @@ def attestations():
707
  except ValueError:
708
  # If not a valid integer, ignore the filter
709
  pass
710
-
711
  if search_filter:
712
  query = query.filter(
713
  db.or_(
@@ -774,6 +775,7 @@ def attestations():
774
  target_types=target_types,
775
  tools=tools,
776
  product_filter=product_filter,
 
777
  target_type_filter=target_type_filter,
778
  tool_filter=tool_filter,
779
  search_filter=search_filter,
@@ -1823,6 +1825,7 @@ def policy_results():
1823
  # Filtering
1824
  rule_id_filter = request.args.get('rule_id', '')
1825
  product_filter = request.args.get('product', '')
 
1826
  result_kind_filter = request.args.get('result_kind', '')
1827
  result_level_filter = request.args.get('result_level', '')
1828
  # Explicitly default show_all_users to False when not provided
@@ -1855,6 +1858,8 @@ def policy_results():
1855
 
1856
  if product_filter and product_filter.strip():
1857
  query = query.filter(PolicyResult.product_key == product_filter.strip())
 
 
1858
 
1859
  if result_kind_filter:
1860
  query = query.filter(PolicyResult.result_kind == result_kind_filter)
@@ -1919,6 +1924,7 @@ def policy_results():
1919
  distinct_rule_count=distinct_rule_count,
1920
  rule_id_filter=rule_id_filter,
1921
  product_filter=product_filter,
 
1922
  result_kind_filter=result_kind_filter,
1923
  result_level_filter=result_level_filter,
1924
  product_keys=product_keys,
@@ -2430,11 +2436,13 @@ def update_product():
2430
  return jsonify({'success': True, 'product': product.to_dict()})
2431
 
2432
  # Product attestations redirect
2433
- @web_bp.route('/products/<product>/attestations')
2434
- def product_attestations(product):
2435
- return redirect(url_for('web.attestations', product=product))
2436
-
2437
- # Product policy results redirect
2438
- @web_bp.route('/products/<product>/policy_results')
2439
- def product_policy_results(product):
2440
- return redirect(url_for('web.policy_results', product=product))
 
 
 
224
  @login_required
225
  def products_page():
226
  filter_query = request.args.get('filter', '')
227
+ version_filter = request.args.get('version', '')
228
  page = request.args.get('page', 1, type=int)
229
  per_page = request.args.get('per_page', 20, type=int)
230
  user_id = session.get('user_id')
 
235
  is_admin = _is_admin_user_web(user_id)
236
  if is_admin:
237
  show_all = request.args.get('show_all', '0') == '1'
238
+ products, pagination = get_products_with_stats(filter_query, version_filter=version_filter, user_id=user_id, show_all=show_all)
239
+ return render_template('products.html', products=products, pagination=pagination, filter_query=filter_query, version_filter=version_filter, show_all=show_all, is_admin=is_admin)
240
 
241
  # --- Products CSV Download ---
242
  @web_bp.route('/products/download-csv')
 
244
  def download_products_csv():
245
  """Download products table as CSV, one line per product version and badge."""
246
  filter_query = request.args.get('filter', '')
247
+ version_filter = request.args.get('version', '')
248
  page = request.args.get('page', 1, type=int)
249
  per_page = request.args.get('per_page', 20, type=int)
250
  custom_filename = request.args.get('filename', '')
251
+ products, pagination = get_products_with_stats(filter_query, version_filter=version_filter)
252
  from models import Badge
253
  all_badges = Badge.query.order_by(Badge.name).all()
254
  output = io.StringIO()
 
679
 
680
  # Get filter parameters
681
  product_filter = request.args.get('product', '')
682
+ version_filter = request.args.get('version', '')
683
  target_type_filter = request.args.get('target_type', '')
684
  tool_filter = request.args.get('tool', '')
685
  search_filter = request.args.get('search', '')
 
696
  # Apply filters
697
  if product_filter and product_filter.strip():
698
  query = query.filter(Attestation.product_key == product_filter.strip())
699
+ if version_filter and version_filter.strip():
700
+ query = query.filter(Attestation.product_version.ilike(f"%{version_filter.strip()}%"))
701
  if target_type_filter:
702
  query = query.filter(Attestation.target_type == target_type_filter)
 
703
  if tool_filter:
704
  query = query.filter(Attestation.tool.ilike(f"%{tool_filter}%"))
 
705
  if id_filter and id_filter.strip():
706
  try:
707
  attestation_id = int(id_filter.strip())
 
709
  except ValueError:
710
  # If not a valid integer, ignore the filter
711
  pass
 
712
  if search_filter:
713
  query = query.filter(
714
  db.or_(
 
775
  target_types=target_types,
776
  tools=tools,
777
  product_filter=product_filter,
778
+ version_filter=version_filter,
779
  target_type_filter=target_type_filter,
780
  tool_filter=tool_filter,
781
  search_filter=search_filter,
 
1825
  # Filtering
1826
  rule_id_filter = request.args.get('rule_id', '')
1827
  product_filter = request.args.get('product', '')
1828
+ version_filter = request.args.get('version', '')
1829
  result_kind_filter = request.args.get('result_kind', '')
1830
  result_level_filter = request.args.get('result_level', '')
1831
  # Explicitly default show_all_users to False when not provided
 
1858
 
1859
  if product_filter and product_filter.strip():
1860
  query = query.filter(PolicyResult.product_key == product_filter.strip())
1861
+ if version_filter and version_filter.strip():
1862
+ query = query.filter(PolicyResult.product_version.ilike(f"%{version_filter.strip()}%"))
1863
 
1864
  if result_kind_filter:
1865
  query = query.filter(PolicyResult.result_kind == result_kind_filter)
 
1924
  distinct_rule_count=distinct_rule_count,
1925
  rule_id_filter=rule_id_filter,
1926
  product_filter=product_filter,
1927
+ version_filter=version_filter,
1928
  result_kind_filter=result_kind_filter,
1929
  result_level_filter=result_level_filter,
1930
  product_keys=product_keys,
 
2436
  return jsonify({'success': True, 'product': product.to_dict()})
2437
 
2438
  # Product attestations redirect
2439
+
2440
+ # Product attestations redirect (with version)
2441
+ @web_bp.route('/products/<product>/<version>/attestations')
2442
+ def product_attestations(product, version):
2443
+ return redirect(url_for('web.attestations', product=product, version=version))
2444
+
2445
+ # Product policy results redirect (with version)
2446
+ @web_bp.route('/products/<product>/<version>/policy_results')
2447
+ def product_policy_results(product, version):
2448
+ return redirect(url_for('web.policy_results', product=product, version=version))