Jeff Vander Stoep | 74e4f93 | 2016-02-08 15:27:10 -0800 | [diff] [blame] | 1 | # Authors: Karl MacMillan <kmacmillan@mentalrootkit.com> |
| 2 | # |
| 3 | # Copyright (C) 2006-2007 Red Hat |
| 4 | # see file 'COPYING' for use and warranty information |
| 5 | # |
| 6 | # This program is free software; you can redistribute it and/or |
| 7 | # modify it under the terms of the GNU General Public License as |
| 8 | # published by the Free Software Foundation; version 2 only |
| 9 | # |
| 10 | # This program is distributed in the hope that it will be useful, |
| 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | # GNU General Public License for more details. |
| 14 | # |
| 15 | # You should have received a copy of the GNU General Public License |
| 16 | # along with this program; if not, write to the Free Software |
| 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 18 | # |
| 19 | |
| 20 | # OVERVIEW |
| 21 | # |
| 22 | # |
| 23 | # This is a parser for the refpolicy policy "language" - i.e., the |
| 24 | # normal SELinux policy language plus the refpolicy style M4 macro |
| 25 | # constructs on top of that base language. This parser is primarily |
| 26 | # aimed at parsing the policy headers in order to create an abstract |
| 27 | # policy representation suitable for generating policy. |
| 28 | # |
| 29 | # Both the lexer and parser are included in this file. The are implemented |
| 30 | # using the Ply library (included with sepolgen). |
| 31 | |
| 32 | import sys |
| 33 | import os |
| 34 | import re |
| 35 | import traceback |
| 36 | |
| 37 | from . import access |
| 38 | from . import defaults |
| 39 | from . import lex |
| 40 | from . import refpolicy |
| 41 | from . import yacc |
| 42 | |
| 43 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: |
| 44 | # |
| 45 | # lexer |
| 46 | # |
| 47 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: |
| 48 | |
| 49 | tokens = ( |
| 50 | # basic tokens, punctuation |
| 51 | 'TICK', |
| 52 | 'SQUOTE', |
| 53 | 'OBRACE', |
| 54 | 'CBRACE', |
| 55 | 'SEMI', |
| 56 | 'COLON', |
| 57 | 'OPAREN', |
| 58 | 'CPAREN', |
| 59 | 'COMMA', |
| 60 | 'MINUS', |
| 61 | 'TILDE', |
| 62 | 'ASTERISK', |
| 63 | 'AMP', |
| 64 | 'BAR', |
| 65 | 'EXPL', |
| 66 | 'EQUAL', |
| 67 | 'FILENAME', |
| 68 | 'IDENTIFIER', |
| 69 | 'NUMBER', |
| 70 | 'PATH', |
| 71 | 'IPV6_ADDR', |
| 72 | # reserved words |
| 73 | # module |
| 74 | 'MODULE', |
| 75 | 'POLICY_MODULE', |
| 76 | 'REQUIRE', |
| 77 | # flask |
| 78 | 'SID', |
| 79 | 'GENFSCON', |
| 80 | 'FS_USE_XATTR', |
| 81 | 'FS_USE_TRANS', |
| 82 | 'FS_USE_TASK', |
| 83 | 'PORTCON', |
| 84 | 'NODECON', |
| 85 | 'NETIFCON', |
| 86 | 'PIRQCON', |
| 87 | 'IOMEMCON', |
| 88 | 'IOPORTCON', |
| 89 | 'PCIDEVICECON', |
| 90 | 'DEVICETREECON', |
| 91 | # object classes |
| 92 | 'CLASS', |
| 93 | # types and attributes |
| 94 | 'TYPEATTRIBUTE', |
| 95 | 'ROLEATTRIBUTE', |
| 96 | 'TYPE', |
| 97 | 'ATTRIBUTE', |
| 98 | 'ATTRIBUTE_ROLE', |
| 99 | 'ALIAS', |
| 100 | 'TYPEALIAS', |
| 101 | # conditional policy |
| 102 | 'BOOL', |
| 103 | 'TRUE', |
| 104 | 'FALSE', |
| 105 | 'IF', |
| 106 | 'ELSE', |
| 107 | # users and roles |
| 108 | 'ROLE', |
| 109 | 'TYPES', |
| 110 | # rules |
| 111 | 'ALLOW', |
| 112 | 'DONTAUDIT', |
| 113 | 'AUDITALLOW', |
| 114 | 'NEVERALLOW', |
| 115 | 'PERMISSIVE', |
| 116 | 'TYPE_TRANSITION', |
| 117 | 'TYPE_CHANGE', |
| 118 | 'TYPE_MEMBER', |
| 119 | 'RANGE_TRANSITION', |
| 120 | 'ROLE_TRANSITION', |
| 121 | # refpolicy keywords |
| 122 | 'OPT_POLICY', |
| 123 | 'INTERFACE', |
| 124 | 'TUNABLE_POLICY', |
| 125 | 'GEN_REQ', |
| 126 | 'TEMPLATE', |
| 127 | 'GEN_CONTEXT', |
| 128 | # m4 |
| 129 | 'IFELSE', |
| 130 | 'IFDEF', |
| 131 | 'IFNDEF', |
| 132 | 'DEFINE' |
| 133 | ) |
| 134 | |
| 135 | # All reserved keywords - see t_IDENTIFIER for how these are matched in |
| 136 | # the lexer. |
| 137 | reserved = { |
| 138 | # module |
| 139 | 'module' : 'MODULE', |
| 140 | 'policy_module' : 'POLICY_MODULE', |
| 141 | 'require' : 'REQUIRE', |
| 142 | # flask |
| 143 | 'sid' : 'SID', |
| 144 | 'genfscon' : 'GENFSCON', |
| 145 | 'fs_use_xattr' : 'FS_USE_XATTR', |
| 146 | 'fs_use_trans' : 'FS_USE_TRANS', |
| 147 | 'fs_use_task' : 'FS_USE_TASK', |
| 148 | 'portcon' : 'PORTCON', |
| 149 | 'nodecon' : 'NODECON', |
| 150 | 'netifcon' : 'NETIFCON', |
| 151 | 'pirqcon' : 'PIRQCON', |
| 152 | 'iomemcon' : 'IOMEMCON', |
| 153 | 'ioportcon' : 'IOPORTCON', |
| 154 | 'pcidevicecon' : 'PCIDEVICECON', |
| 155 | 'devicetreecon' : 'DEVICETREECON', |
| 156 | # object classes |
| 157 | 'class' : 'CLASS', |
| 158 | # types and attributes |
| 159 | 'typeattribute' : 'TYPEATTRIBUTE', |
| 160 | 'roleattribute' : 'ROLEATTRIBUTE', |
| 161 | 'type' : 'TYPE', |
| 162 | 'attribute' : 'ATTRIBUTE', |
| 163 | 'attribute_role' : 'ATTRIBUTE_ROLE', |
| 164 | 'alias' : 'ALIAS', |
| 165 | 'typealias' : 'TYPEALIAS', |
| 166 | # conditional policy |
| 167 | 'bool' : 'BOOL', |
| 168 | 'true' : 'TRUE', |
| 169 | 'false' : 'FALSE', |
| 170 | 'if' : 'IF', |
| 171 | 'else' : 'ELSE', |
| 172 | # users and roles |
| 173 | 'role' : 'ROLE', |
| 174 | 'types' : 'TYPES', |
| 175 | # rules |
| 176 | 'allow' : 'ALLOW', |
| 177 | 'dontaudit' : 'DONTAUDIT', |
| 178 | 'auditallow' : 'AUDITALLOW', |
| 179 | 'neverallow' : 'NEVERALLOW', |
| 180 | 'permissive' : 'PERMISSIVE', |
| 181 | 'type_transition' : 'TYPE_TRANSITION', |
| 182 | 'type_change' : 'TYPE_CHANGE', |
| 183 | 'type_member' : 'TYPE_MEMBER', |
| 184 | 'range_transition' : 'RANGE_TRANSITION', |
| 185 | 'role_transition' : 'ROLE_TRANSITION', |
| 186 | # refpolicy keywords |
| 187 | 'optional_policy' : 'OPT_POLICY', |
| 188 | 'interface' : 'INTERFACE', |
| 189 | 'tunable_policy' : 'TUNABLE_POLICY', |
| 190 | 'gen_require' : 'GEN_REQ', |
| 191 | 'template' : 'TEMPLATE', |
| 192 | 'gen_context' : 'GEN_CONTEXT', |
| 193 | # M4 |
| 194 | 'ifelse' : 'IFELSE', |
| 195 | 'ifndef' : 'IFNDEF', |
| 196 | 'ifdef' : 'IFDEF', |
| 197 | 'define' : 'DEFINE' |
| 198 | } |
| 199 | |
| 200 | # The ply lexer allows definition of tokens in 2 ways: regular expressions |
| 201 | # or functions. |
| 202 | |
| 203 | # Simple regex tokens |
| 204 | t_TICK = r'\`' |
| 205 | t_SQUOTE = r'\'' |
| 206 | t_OBRACE = r'\{' |
| 207 | t_CBRACE = r'\}' |
| 208 | # This will handle spurios extra ';' via the + |
| 209 | t_SEMI = r'\;+' |
| 210 | t_COLON = r'\:' |
| 211 | t_OPAREN = r'\(' |
| 212 | t_CPAREN = r'\)' |
| 213 | t_COMMA = r'\,' |
| 214 | t_MINUS = r'\-' |
| 215 | t_TILDE = r'\~' |
| 216 | t_ASTERISK = r'\*' |
| 217 | t_AMP = r'\&' |
| 218 | t_BAR = r'\|' |
| 219 | t_EXPL = r'\!' |
| 220 | t_EQUAL = r'\=' |
| 221 | t_NUMBER = r'[0-9\.]+' |
| 222 | t_PATH = r'/[a-zA-Z0-9)_\.\*/\$]*' |
| 223 | #t_IPV6_ADDR = r'[a-fA-F0-9]{0,4}:[a-fA-F0-9]{0,4}:([a-fA-F0-9]{0,4}:)*' |
| 224 | |
| 225 | # Ignore whitespace - this is a special token for ply that more efficiently |
| 226 | # ignores uninteresting tokens. |
| 227 | t_ignore = " \t" |
| 228 | |
| 229 | # More complex tokens |
| 230 | def t_IPV6_ADDR(t): |
| 231 | r'[a-fA-F0-9]{0,4}:[a-fA-F0-9]{0,4}:([a-fA-F0-9]|:)*' |
| 232 | # This is a function simply to force it sooner into |
| 233 | # the regex list |
| 234 | return t |
| 235 | |
| 236 | def t_m4comment(t): |
| 237 | r'dnl.*\n' |
| 238 | # Ignore all comments |
| 239 | t.lexer.lineno += 1 |
| 240 | |
| 241 | def t_refpolicywarn1(t): |
| 242 | r'define.*refpolicywarn\(.*\n' |
| 243 | # Ignore refpolicywarn statements - they sometimes |
| 244 | # contain text that we can't parse. |
| 245 | t.skip(1) |
| 246 | |
| 247 | def t_refpolicywarn(t): |
| 248 | r'refpolicywarn\(.*\n' |
| 249 | # Ignore refpolicywarn statements - they sometimes |
| 250 | # contain text that we can't parse. |
| 251 | t.lexer.lineno += 1 |
| 252 | |
| 253 | def t_IDENTIFIER(t): |
| 254 | r'[a-zA-Z_\$][a-zA-Z0-9_\-\+\.\$\*~]*' |
| 255 | # Handle any keywords |
| 256 | t.type = reserved.get(t.value,'IDENTIFIER') |
| 257 | return t |
| 258 | |
| 259 | def t_FILENAME(t): |
| 260 | r'\"[a-zA-Z0-9_\-\+\.\$\*~ :]+\"' |
| 261 | # Handle any keywords |
| 262 | t.type = reserved.get(t.value,'FILENAME') |
| 263 | return t |
| 264 | |
| 265 | def t_comment(t): |
| 266 | r'\#.*\n' |
| 267 | # Ignore all comments |
| 268 | t.lexer.lineno += 1 |
| 269 | |
| 270 | def t_error(t): |
| 271 | print("Illegal character '%s'" % t.value[0]) |
| 272 | t.skip(1) |
| 273 | |
| 274 | def t_newline(t): |
| 275 | r'\n+' |
| 276 | t.lexer.lineno += len(t.value) |
| 277 | |
| 278 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: |
| 279 | # |
| 280 | # Parser |
| 281 | # |
| 282 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: |
| 283 | |
| 284 | # Global data used during parsing - making it global is easier than |
| 285 | # passing the state through the parsing functions. |
| 286 | |
| 287 | # m is the top-level data structure (stands for modules). |
| 288 | m = None |
| 289 | # error is either None (indicating no error) or a string error message. |
| 290 | error = None |
| 291 | parse_file = "" |
| 292 | # spt is the support macros (e.g., obj/perm sets) - it is an instance of |
| 293 | # refpolicy.SupportMacros and should always be present during parsing |
| 294 | # though it may not contain any macros. |
| 295 | spt = None |
| 296 | success = True |
| 297 | |
| 298 | # utilities |
| 299 | def collect(stmts, parent, val=None): |
| 300 | if stmts is None: |
| 301 | return |
| 302 | for s in stmts: |
| 303 | if s is None: |
| 304 | continue |
| 305 | s.parent = parent |
| 306 | if val is not None: |
| 307 | parent.children.insert(0, (val, s)) |
| 308 | else: |
| 309 | parent.children.insert(0, s) |
| 310 | |
| 311 | def expand(ids, s): |
| 312 | for id in ids: |
| 313 | if spt.has_key(id): |
| 314 | s.update(spt.by_name(id)) |
| 315 | else: |
| 316 | s.add(id) |
| 317 | |
| 318 | # Top-level non-terminal |
| 319 | def p_statements(p): |
| 320 | '''statements : statement |
| 321 | | statements statement |
| 322 | | empty |
| 323 | ''' |
| 324 | if len(p) == 2 and p[1]: |
| 325 | m.children.append(p[1]) |
| 326 | elif len(p) > 2 and p[2]: |
| 327 | m.children.append(p[2]) |
| 328 | |
| 329 | def p_statement(p): |
| 330 | '''statement : interface |
| 331 | | template |
| 332 | | obj_perm_set |
| 333 | | policy |
| 334 | | policy_module_stmt |
| 335 | | module_stmt |
| 336 | ''' |
| 337 | p[0] = p[1] |
| 338 | |
| 339 | def p_empty(p): |
| 340 | 'empty :' |
| 341 | pass |
| 342 | |
| 343 | # |
| 344 | # Reference policy language constructs |
| 345 | # |
| 346 | |
| 347 | # This is for the policy module statement (e.g., policy_module(foo,1.2.0)). |
| 348 | # We have a separate terminal for either the basic language module statement |
| 349 | # and interface calls to make it easier to identifier. |
| 350 | def p_policy_module_stmt(p): |
| 351 | 'policy_module_stmt : POLICY_MODULE OPAREN IDENTIFIER COMMA NUMBER CPAREN' |
| 352 | m = refpolicy.ModuleDeclaration() |
| 353 | m.name = p[3] |
| 354 | m.version = p[5] |
| 355 | m.refpolicy = True |
| 356 | p[0] = m |
| 357 | |
| 358 | def p_interface(p): |
| 359 | '''interface : INTERFACE OPAREN TICK IDENTIFIER SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN |
| 360 | ''' |
| 361 | x = refpolicy.Interface(p[4]) |
| 362 | collect(p[8], x) |
| 363 | p[0] = x |
| 364 | |
| 365 | def p_template(p): |
| 366 | '''template : TEMPLATE OPAREN TICK IDENTIFIER SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN |
| 367 | | DEFINE OPAREN TICK IDENTIFIER SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN |
| 368 | ''' |
| 369 | x = refpolicy.Template(p[4]) |
| 370 | collect(p[8], x) |
| 371 | p[0] = x |
| 372 | |
| 373 | def p_define(p): |
| 374 | '''define : DEFINE OPAREN TICK IDENTIFIER SQUOTE CPAREN''' |
| 375 | # This is for defining single M4 values (to be used later in ifdef statements). |
| 376 | # Example: define(`sulogin_no_pam'). We don't currently do anything with these |
| 377 | # but we should in the future when we correctly resolve ifdef statements. |
| 378 | p[0] = None |
| 379 | |
| 380 | def p_interface_stmts(p): |
| 381 | '''interface_stmts : policy |
| 382 | | interface_stmts policy |
| 383 | | empty |
| 384 | ''' |
| 385 | if len(p) == 2 and p[1]: |
| 386 | p[0] = p[1] |
| 387 | elif len(p) > 2: |
| 388 | if not p[1]: |
| 389 | if p[2]: |
| 390 | p[0] = p[2] |
| 391 | elif not p[2]: |
| 392 | p[0] = p[1] |
| 393 | else: |
| 394 | p[0] = p[1] + p[2] |
| 395 | |
| 396 | def p_optional_policy(p): |
| 397 | '''optional_policy : OPT_POLICY OPAREN TICK interface_stmts SQUOTE CPAREN |
| 398 | | OPT_POLICY OPAREN TICK interface_stmts SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN |
| 399 | ''' |
| 400 | o = refpolicy.OptionalPolicy() |
| 401 | collect(p[4], o, val=True) |
| 402 | if len(p) > 7: |
| 403 | collect(p[8], o, val=False) |
| 404 | p[0] = [o] |
| 405 | |
| 406 | def p_tunable_policy(p): |
| 407 | '''tunable_policy : TUNABLE_POLICY OPAREN TICK cond_expr SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN |
| 408 | | TUNABLE_POLICY OPAREN TICK cond_expr SQUOTE COMMA TICK interface_stmts SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN |
| 409 | ''' |
| 410 | x = refpolicy.TunablePolicy() |
| 411 | x.cond_expr = p[4] |
| 412 | collect(p[8], x, val=True) |
| 413 | if len(p) > 11: |
| 414 | collect(p[12], x, val=False) |
| 415 | p[0] = [x] |
| 416 | |
| 417 | def p_ifelse(p): |
| 418 | '''ifelse : IFELSE OPAREN TICK IDENTIFIER SQUOTE COMMA COMMA TICK IDENTIFIER SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN optional_semi |
| 419 | | IFELSE OPAREN TICK IDENTIFIER SQUOTE COMMA TICK IDENTIFIER SQUOTE COMMA TICK interface_stmts SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN optional_semi |
| 420 | | IFELSE OPAREN TICK IDENTIFIER SQUOTE COMMA TICK SQUOTE COMMA TICK interface_stmts SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN optional_semi |
| 421 | ''' |
| 422 | # x = refpolicy.IfDef(p[4]) |
| 423 | # v = True |
| 424 | # collect(p[8], x, val=v) |
| 425 | # if len(p) > 12: |
| 426 | # collect(p[12], x, val=False) |
| 427 | # p[0] = [x] |
| 428 | pass |
| 429 | |
| 430 | |
| 431 | def p_ifdef(p): |
| 432 | '''ifdef : IFDEF OPAREN TICK IDENTIFIER SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN optional_semi |
| 433 | | IFNDEF OPAREN TICK IDENTIFIER SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN optional_semi |
| 434 | | IFDEF OPAREN TICK IDENTIFIER SQUOTE COMMA TICK interface_stmts SQUOTE COMMA TICK interface_stmts SQUOTE CPAREN optional_semi |
| 435 | ''' |
| 436 | x = refpolicy.IfDef(p[4]) |
| 437 | if p[1] == 'ifdef': |
| 438 | v = True |
| 439 | else: |
| 440 | v = False |
| 441 | collect(p[8], x, val=v) |
| 442 | if len(p) > 12: |
| 443 | collect(p[12], x, val=False) |
| 444 | p[0] = [x] |
| 445 | |
| 446 | def p_interface_call(p): |
| 447 | '''interface_call : IDENTIFIER OPAREN interface_call_param_list CPAREN |
| 448 | | IDENTIFIER OPAREN CPAREN |
| 449 | | IDENTIFIER OPAREN interface_call_param_list CPAREN SEMI''' |
| 450 | # Allow spurious semi-colons at the end of interface calls |
| 451 | i = refpolicy.InterfaceCall(ifname=p[1]) |
| 452 | if len(p) > 4: |
| 453 | i.args.extend(p[3]) |
| 454 | p[0] = i |
| 455 | |
| 456 | def p_interface_call_param(p): |
| 457 | '''interface_call_param : IDENTIFIER |
| 458 | | IDENTIFIER MINUS IDENTIFIER |
| 459 | | nested_id_set |
| 460 | | TRUE |
| 461 | | FALSE |
| 462 | | FILENAME |
| 463 | ''' |
| 464 | # Intentionally let single identifiers pass through |
| 465 | # List means set, non-list identifier |
| 466 | if len(p) == 2: |
| 467 | p[0] = p[1] |
| 468 | else: |
| 469 | p[0] = [p[1], "-" + p[3]] |
| 470 | |
| 471 | def p_interface_call_param_list(p): |
| 472 | '''interface_call_param_list : interface_call_param |
| 473 | | interface_call_param_list COMMA interface_call_param |
| 474 | ''' |
| 475 | if len(p) == 2: |
| 476 | p[0] = [p[1]] |
| 477 | else: |
| 478 | p[0] = p[1] + [p[3]] |
| 479 | |
| 480 | |
| 481 | def p_obj_perm_set(p): |
| 482 | 'obj_perm_set : DEFINE OPAREN TICK IDENTIFIER SQUOTE COMMA TICK names SQUOTE CPAREN' |
| 483 | s = refpolicy.ObjPermSet(p[4]) |
| 484 | s.perms = p[8] |
| 485 | p[0] = s |
| 486 | |
| 487 | # |
| 488 | # Basic SELinux policy language |
| 489 | # |
| 490 | |
| 491 | def p_policy(p): |
| 492 | '''policy : policy_stmt |
| 493 | | optional_policy |
| 494 | | tunable_policy |
| 495 | | ifdef |
| 496 | | ifelse |
| 497 | | conditional |
| 498 | ''' |
| 499 | p[0] = p[1] |
| 500 | |
| 501 | def p_policy_stmt(p): |
| 502 | '''policy_stmt : gen_require |
| 503 | | avrule_def |
| 504 | | typerule_def |
| 505 | | typeattribute_def |
| 506 | | roleattribute_def |
| 507 | | interface_call |
| 508 | | role_def |
| 509 | | role_allow |
| 510 | | permissive |
| 511 | | type_def |
| 512 | | typealias_def |
| 513 | | attribute_def |
| 514 | | attribute_role_def |
| 515 | | range_transition_def |
| 516 | | role_transition_def |
| 517 | | bool |
| 518 | | define |
| 519 | | initial_sid |
| 520 | | genfscon |
| 521 | | fs_use |
| 522 | | portcon |
| 523 | | nodecon |
| 524 | | netifcon |
| 525 | | pirqcon |
| 526 | | iomemcon |
| 527 | | ioportcon |
| 528 | | pcidevicecon |
| 529 | | devicetreecon |
| 530 | ''' |
| 531 | if p[1]: |
| 532 | p[0] = [p[1]] |
| 533 | |
| 534 | def p_module_stmt(p): |
| 535 | 'module_stmt : MODULE IDENTIFIER NUMBER SEMI' |
| 536 | m = refpolicy.ModuleDeclaration() |
| 537 | m.name = p[2] |
| 538 | m.version = p[3] |
| 539 | m.refpolicy = False |
| 540 | p[0] = m |
| 541 | |
| 542 | def p_gen_require(p): |
| 543 | '''gen_require : GEN_REQ OPAREN TICK requires SQUOTE CPAREN |
| 544 | | REQUIRE OBRACE requires CBRACE''' |
| 545 | # We ignore the require statements - they are redundant data from our point-of-view. |
| 546 | # Checkmodule will verify them later anyway so we just assume that they match what |
| 547 | # is in the rest of the interface. |
| 548 | pass |
| 549 | |
| 550 | def p_requires(p): |
| 551 | '''requires : require |
| 552 | | requires require |
| 553 | | ifdef |
| 554 | | requires ifdef |
| 555 | ''' |
| 556 | pass |
| 557 | |
| 558 | def p_require(p): |
| 559 | '''require : TYPE comma_list SEMI |
| 560 | | ROLE comma_list SEMI |
| 561 | | ATTRIBUTE comma_list SEMI |
| 562 | | ATTRIBUTE_ROLE comma_list SEMI |
| 563 | | CLASS comma_list SEMI |
| 564 | | BOOL comma_list SEMI |
| 565 | ''' |
| 566 | pass |
| 567 | |
| 568 | def p_security_context(p): |
| 569 | '''security_context : IDENTIFIER COLON IDENTIFIER COLON IDENTIFIER |
| 570 | | IDENTIFIER COLON IDENTIFIER COLON IDENTIFIER COLON mls_range_def''' |
| 571 | # This will likely need some updates to handle complex levels |
| 572 | s = refpolicy.SecurityContext() |
| 573 | s.user = p[1] |
| 574 | s.role = p[3] |
| 575 | s.type = p[5] |
| 576 | if len(p) > 6: |
| 577 | s.level = p[7] |
| 578 | |
| 579 | p[0] = s |
| 580 | |
| 581 | def p_gen_context(p): |
| 582 | '''gen_context : GEN_CONTEXT OPAREN security_context COMMA mls_range_def CPAREN |
| 583 | ''' |
| 584 | # We actually store gen_context statements in a SecurityContext |
| 585 | # object - it knows how to output either a bare context or a |
| 586 | # gen_context statement. |
| 587 | s = p[3] |
| 588 | s.level = p[5] |
| 589 | |
| 590 | p[0] = s |
| 591 | |
| 592 | def p_context(p): |
| 593 | '''context : security_context |
| 594 | | gen_context |
| 595 | ''' |
| 596 | p[0] = p[1] |
| 597 | |
| 598 | def p_initial_sid(p): |
| 599 | '''initial_sid : SID IDENTIFIER context''' |
| 600 | s = refpolicy.InitialSid() |
| 601 | s.name = p[2] |
| 602 | s.context = p[3] |
| 603 | p[0] = s |
| 604 | |
| 605 | def p_genfscon(p): |
| 606 | '''genfscon : GENFSCON IDENTIFIER PATH context''' |
| 607 | |
| 608 | g = refpolicy.GenfsCon() |
| 609 | g.filesystem = p[2] |
| 610 | g.path = p[3] |
| 611 | g.context = p[4] |
| 612 | |
| 613 | p[0] = g |
| 614 | |
| 615 | def p_fs_use(p): |
| 616 | '''fs_use : FS_USE_XATTR IDENTIFIER context SEMI |
| 617 | | FS_USE_TASK IDENTIFIER context SEMI |
| 618 | | FS_USE_TRANS IDENTIFIER context SEMI |
| 619 | ''' |
| 620 | f = refpolicy.FilesystemUse() |
| 621 | if p[1] == "fs_use_xattr": |
| 622 | f.type = refpolicy.FilesystemUse.XATTR |
| 623 | elif p[1] == "fs_use_task": |
| 624 | f.type = refpolicy.FilesystemUse.TASK |
| 625 | elif p[1] == "fs_use_trans": |
| 626 | f.type = refpolicy.FilesystemUse.TRANS |
| 627 | |
| 628 | f.filesystem = p[2] |
| 629 | f.context = p[3] |
| 630 | |
| 631 | p[0] = f |
| 632 | |
| 633 | def p_portcon(p): |
| 634 | '''portcon : PORTCON IDENTIFIER NUMBER context |
| 635 | | PORTCON IDENTIFIER NUMBER MINUS NUMBER context''' |
| 636 | c = refpolicy.PortCon() |
| 637 | c.port_type = p[2] |
| 638 | if len(p) == 5: |
| 639 | c.port_number = p[3] |
| 640 | c.context = p[4] |
| 641 | else: |
| 642 | c.port_number = p[3] + "-" + p[4] |
| 643 | c.context = p[5] |
| 644 | |
| 645 | p[0] = c |
| 646 | |
| 647 | def p_nodecon(p): |
| 648 | '''nodecon : NODECON NUMBER NUMBER context |
| 649 | | NODECON IPV6_ADDR IPV6_ADDR context |
| 650 | ''' |
| 651 | n = refpolicy.NodeCon() |
| 652 | n.start = p[2] |
| 653 | n.end = p[3] |
| 654 | n.context = p[4] |
| 655 | |
| 656 | p[0] = n |
| 657 | |
| 658 | def p_netifcon(p): |
| 659 | 'netifcon : NETIFCON IDENTIFIER context context' |
| 660 | n = refpolicy.NetifCon() |
| 661 | n.interface = p[2] |
| 662 | n.interface_context = p[3] |
| 663 | n.packet_context = p[4] |
| 664 | |
| 665 | p[0] = n |
| 666 | |
| 667 | def p_pirqcon(p): |
| 668 | 'pirqcon : PIRQCON NUMBER context' |
| 669 | c = refpolicy.PirqCon() |
| 670 | c.pirq_number = p[2] |
| 671 | c.context = p[3] |
| 672 | |
| 673 | p[0] = c |
| 674 | |
| 675 | def p_iomemcon(p): |
| 676 | '''iomemcon : IOMEMCON NUMBER context |
| 677 | | IOMEMCON NUMBER MINUS NUMBER context''' |
| 678 | c = refpolicy.IomemCon() |
| 679 | if len(p) == 4: |
| 680 | c.device_mem = p[2] |
| 681 | c.context = p[3] |
| 682 | else: |
| 683 | c.device_mem = p[2] + "-" + p[3] |
| 684 | c.context = p[4] |
| 685 | |
| 686 | p[0] = c |
| 687 | |
| 688 | def p_ioportcon(p): |
| 689 | '''ioportcon : IOPORTCON NUMBER context |
| 690 | | IOPORTCON NUMBER MINUS NUMBER context''' |
| 691 | c = refpolicy.IoportCon() |
| 692 | if len(p) == 4: |
| 693 | c.ioport = p[2] |
| 694 | c.context = p[3] |
| 695 | else: |
| 696 | c.ioport = p[2] + "-" + p[3] |
| 697 | c.context = p[4] |
| 698 | |
| 699 | p[0] = c |
| 700 | |
| 701 | def p_pcidevicecon(p): |
| 702 | 'pcidevicecon : PCIDEVICECON NUMBER context' |
| 703 | c = refpolicy.PciDeviceCon() |
| 704 | c.device = p[2] |
| 705 | c.context = p[3] |
| 706 | |
| 707 | p[0] = c |
| 708 | |
| 709 | def p_devicetreecon(p): |
| 710 | 'devicetreecon : DEVICETREECON NUMBER context' |
| 711 | c = refpolicy.DevicetTeeCon() |
| 712 | c.path = p[2] |
| 713 | c.context = p[3] |
| 714 | |
| 715 | p[0] = c |
| 716 | |
| 717 | def p_mls_range_def(p): |
| 718 | '''mls_range_def : mls_level_def MINUS mls_level_def |
| 719 | | mls_level_def |
| 720 | ''' |
| 721 | p[0] = p[1] |
| 722 | if len(p) > 2: |
| 723 | p[0] = p[0] + "-" + p[3] |
| 724 | |
| 725 | def p_mls_level_def(p): |
| 726 | '''mls_level_def : IDENTIFIER COLON comma_list |
| 727 | | IDENTIFIER |
| 728 | ''' |
| 729 | p[0] = p[1] |
| 730 | if len(p) > 2: |
| 731 | p[0] = p[0] + ":" + ",".join(p[3]) |
| 732 | |
| 733 | def p_type_def(p): |
| 734 | '''type_def : TYPE IDENTIFIER COMMA comma_list SEMI |
| 735 | | TYPE IDENTIFIER SEMI |
| 736 | | TYPE IDENTIFIER ALIAS names SEMI |
| 737 | | TYPE IDENTIFIER ALIAS names COMMA comma_list SEMI |
| 738 | ''' |
| 739 | t = refpolicy.Type(p[2]) |
| 740 | if len(p) == 6: |
| 741 | if p[3] == ',': |
| 742 | t.attributes.update(p[4]) |
| 743 | else: |
| 744 | t.aliases = p[4] |
| 745 | elif len(p) > 4: |
| 746 | t.aliases = p[4] |
| 747 | if len(p) == 8: |
| 748 | t.attributes.update(p[6]) |
| 749 | p[0] = t |
| 750 | |
| 751 | def p_attribute_def(p): |
| 752 | 'attribute_def : ATTRIBUTE IDENTIFIER SEMI' |
| 753 | a = refpolicy.Attribute(p[2]) |
| 754 | p[0] = a |
| 755 | |
| 756 | def p_attribute_role_def(p): |
| 757 | 'attribute_role_def : ATTRIBUTE_ROLE IDENTIFIER SEMI' |
| 758 | a = refpolicy.Attribute_Role(p[2]) |
| 759 | p[0] = a |
| 760 | |
| 761 | def p_typealias_def(p): |
| 762 | 'typealias_def : TYPEALIAS IDENTIFIER ALIAS names SEMI' |
| 763 | t = refpolicy.TypeAlias() |
| 764 | t.type = p[2] |
| 765 | t.aliases = p[4] |
| 766 | p[0] = t |
| 767 | |
| 768 | def p_role_def(p): |
| 769 | '''role_def : ROLE IDENTIFIER TYPES comma_list SEMI |
| 770 | | ROLE IDENTIFIER SEMI''' |
| 771 | r = refpolicy.Role() |
| 772 | r.role = p[2] |
| 773 | if len(p) > 4: |
| 774 | r.types.update(p[4]) |
| 775 | p[0] = r |
| 776 | |
| 777 | def p_role_allow(p): |
| 778 | 'role_allow : ALLOW names names SEMI' |
| 779 | r = refpolicy.RoleAllow() |
| 780 | r.src_roles = p[2] |
| 781 | r.tgt_roles = p[3] |
| 782 | p[0] = r |
| 783 | |
| 784 | def p_permissive(p): |
| 785 | 'permissive : PERMISSIVE names SEMI' |
| 786 | t.skip(1) |
| 787 | |
| 788 | def p_avrule_def(p): |
| 789 | '''avrule_def : ALLOW names names COLON names names SEMI |
| 790 | | DONTAUDIT names names COLON names names SEMI |
| 791 | | AUDITALLOW names names COLON names names SEMI |
| 792 | | NEVERALLOW names names COLON names names SEMI |
| 793 | ''' |
| 794 | a = refpolicy.AVRule() |
| 795 | if p[1] == 'dontaudit': |
| 796 | a.rule_type = refpolicy.AVRule.DONTAUDIT |
| 797 | elif p[1] == 'auditallow': |
| 798 | a.rule_type = refpolicy.AVRule.AUDITALLOW |
| 799 | elif p[1] == 'neverallow': |
| 800 | a.rule_type = refpolicy.AVRule.NEVERALLOW |
| 801 | a.src_types = p[2] |
| 802 | a.tgt_types = p[3] |
| 803 | a.obj_classes = p[5] |
| 804 | a.perms = p[6] |
| 805 | p[0] = a |
| 806 | |
| 807 | def p_typerule_def(p): |
| 808 | '''typerule_def : TYPE_TRANSITION names names COLON names IDENTIFIER SEMI |
| 809 | | TYPE_TRANSITION names names COLON names IDENTIFIER FILENAME SEMI |
| 810 | | TYPE_TRANSITION names names COLON names IDENTIFIER IDENTIFIER SEMI |
| 811 | | TYPE_CHANGE names names COLON names IDENTIFIER SEMI |
| 812 | | TYPE_MEMBER names names COLON names IDENTIFIER SEMI |
| 813 | ''' |
| 814 | t = refpolicy.TypeRule() |
| 815 | if p[1] == 'type_change': |
| 816 | t.rule_type = refpolicy.TypeRule.TYPE_CHANGE |
| 817 | elif p[1] == 'type_member': |
| 818 | t.rule_type = refpolicy.TypeRule.TYPE_MEMBER |
| 819 | t.src_types = p[2] |
| 820 | t.tgt_types = p[3] |
| 821 | t.obj_classes = p[5] |
| 822 | t.dest_type = p[6] |
| 823 | t.file_name = p[7] |
| 824 | p[0] = t |
| 825 | |
| 826 | def p_bool(p): |
| 827 | '''bool : BOOL IDENTIFIER TRUE SEMI |
| 828 | | BOOL IDENTIFIER FALSE SEMI''' |
| 829 | b = refpolicy.Bool() |
| 830 | b.name = p[2] |
| 831 | if p[3] == "true": |
| 832 | b.state = True |
| 833 | else: |
| 834 | b.state = False |
| 835 | p[0] = b |
| 836 | |
| 837 | def p_conditional(p): |
| 838 | ''' conditional : IF OPAREN cond_expr CPAREN OBRACE interface_stmts CBRACE |
| 839 | | IF OPAREN cond_expr CPAREN OBRACE interface_stmts CBRACE ELSE OBRACE interface_stmts CBRACE |
| 840 | ''' |
| 841 | c = refpolicy.Conditional() |
| 842 | c.cond_expr = p[3] |
| 843 | collect(p[6], c, val=True) |
| 844 | if len(p) > 8: |
| 845 | collect(p[10], c, val=False) |
| 846 | p[0] = [c] |
| 847 | |
| 848 | def p_typeattribute_def(p): |
| 849 | '''typeattribute_def : TYPEATTRIBUTE IDENTIFIER comma_list SEMI''' |
| 850 | t = refpolicy.TypeAttribute() |
| 851 | t.type = p[2] |
| 852 | t.attributes.update(p[3]) |
| 853 | p[0] = t |
| 854 | |
| 855 | def p_roleattribute_def(p): |
| 856 | '''roleattribute_def : ROLEATTRIBUTE IDENTIFIER comma_list SEMI''' |
| 857 | t = refpolicy.RoleAttribute() |
| 858 | t.role = p[2] |
| 859 | t.roleattributes.update(p[3]) |
| 860 | p[0] = t |
| 861 | |
| 862 | def p_range_transition_def(p): |
| 863 | '''range_transition_def : RANGE_TRANSITION names names COLON names mls_range_def SEMI |
| 864 | | RANGE_TRANSITION names names names SEMI''' |
| 865 | pass |
| 866 | |
| 867 | def p_role_transition_def(p): |
| 868 | '''role_transition_def : ROLE_TRANSITION names names names SEMI''' |
| 869 | pass |
| 870 | |
| 871 | def p_cond_expr(p): |
| 872 | '''cond_expr : IDENTIFIER |
| 873 | | EXPL cond_expr |
| 874 | | cond_expr AMP AMP cond_expr |
| 875 | | cond_expr BAR BAR cond_expr |
| 876 | | cond_expr EQUAL EQUAL cond_expr |
| 877 | | cond_expr EXPL EQUAL cond_expr |
| 878 | ''' |
| 879 | l = len(p) |
| 880 | if l == 2: |
| 881 | p[0] = [p[1]] |
| 882 | elif l == 3: |
| 883 | p[0] = [p[1]] + p[2] |
| 884 | else: |
| 885 | p[0] = p[1] + [p[2] + p[3]] + p[4] |
| 886 | |
| 887 | |
| 888 | # |
| 889 | # Basic terminals |
| 890 | # |
| 891 | |
| 892 | # Identifiers and lists of identifiers. These must |
| 893 | # be handled somewhat gracefully. Names returns an IdSet and care must |
| 894 | # be taken that this is _assigned_ to an object to correctly update |
| 895 | # all of the flags (as opposed to using update). The other terminals |
| 896 | # return list - this is to preserve ordering if it is important for |
| 897 | # parsing (for example, interface_call must retain the ordering). Other |
| 898 | # times the list should be used to update an IdSet. |
| 899 | |
| 900 | def p_names(p): |
| 901 | '''names : identifier |
| 902 | | nested_id_set |
| 903 | | asterisk |
| 904 | | TILDE identifier |
| 905 | | TILDE nested_id_set |
| 906 | | IDENTIFIER MINUS IDENTIFIER |
| 907 | ''' |
| 908 | s = refpolicy.IdSet() |
| 909 | if len(p) < 3: |
| 910 | expand(p[1], s) |
| 911 | elif len(p) == 3: |
| 912 | expand(p[2], s) |
| 913 | s.compliment = True |
| 914 | else: |
| 915 | expand([p[1]]) |
| 916 | s.add("-" + p[3]) |
| 917 | p[0] = s |
| 918 | |
| 919 | def p_identifier(p): |
| 920 | 'identifier : IDENTIFIER' |
| 921 | p[0] = [p[1]] |
| 922 | |
| 923 | def p_asterisk(p): |
| 924 | 'asterisk : ASTERISK' |
| 925 | p[0] = [p[1]] |
| 926 | |
| 927 | def p_nested_id_set(p): |
| 928 | '''nested_id_set : OBRACE nested_id_list CBRACE |
| 929 | ''' |
| 930 | p[0] = p[2] |
| 931 | |
| 932 | def p_nested_id_list(p): |
| 933 | '''nested_id_list : nested_id_element |
| 934 | | nested_id_list nested_id_element |
| 935 | ''' |
| 936 | if len(p) == 2: |
| 937 | p[0] = p[1] |
| 938 | else: |
| 939 | p[0] = p[1] + p[2] |
| 940 | |
| 941 | def p_nested_id_element(p): |
| 942 | '''nested_id_element : identifier |
| 943 | | MINUS IDENTIFIER |
| 944 | | nested_id_set |
| 945 | ''' |
| 946 | if len(p) == 2: |
| 947 | p[0] = p[1] |
| 948 | else: |
| 949 | # For now just leave the '-' |
| 950 | str = "-" + p[2] |
| 951 | p[0] = [str] |
| 952 | |
| 953 | def p_comma_list(p): |
| 954 | '''comma_list : nested_id_list |
| 955 | | comma_list COMMA nested_id_list |
| 956 | ''' |
| 957 | if len(p) > 2: |
| 958 | p[1] = p[1] + p[3] |
| 959 | p[0] = p[1] |
| 960 | |
| 961 | def p_optional_semi(p): |
| 962 | '''optional_semi : SEMI |
| 963 | | empty''' |
| 964 | pass |
| 965 | |
| 966 | |
| 967 | # |
| 968 | # Interface to the parser |
| 969 | # |
| 970 | |
| 971 | def p_error(tok): |
| 972 | global error, parse_file, success, parser |
| 973 | error = "%s: Syntax error on line %d %s [type=%s]" % (parse_file, tok.lineno, tok.value, tok.type) |
| 974 | print(error) |
| 975 | success = False |
| 976 | |
| 977 | def prep_spt(spt): |
| 978 | if not spt: |
| 979 | return { } |
| 980 | map = {} |
| 981 | for x in spt: |
| 982 | map[x.name] = x |
| 983 | |
| 984 | parser = None |
| 985 | lexer = None |
| 986 | def create_globals(module, support, debug): |
| 987 | global parser, lexer, m, spt |
| 988 | |
| 989 | if not parser: |
| 990 | lexer = lex.lex() |
| 991 | parser = yacc.yacc(method="LALR", debug=debug, write_tables=0) |
| 992 | |
| 993 | if module is not None: |
| 994 | m = module |
| 995 | else: |
| 996 | m = refpolicy.Module() |
| 997 | |
| 998 | if not support: |
| 999 | spt = refpolicy.SupportMacros() |
| 1000 | else: |
| 1001 | spt = support |
| 1002 | |
| 1003 | def parse(text, module=None, support=None, debug=False): |
| 1004 | create_globals(module, support, debug) |
| 1005 | global error, parser, lexer, success |
| 1006 | |
| 1007 | lexer.lineno = 1 |
| 1008 | success = True |
| 1009 | |
| 1010 | try: |
| 1011 | parser.parse(text, debug=debug, lexer=lexer) |
| 1012 | except Exception as e: |
| 1013 | parser = None |
| 1014 | lexer = None |
| 1015 | error = "internal parser error: %s" % str(e) + "\n" + traceback.format_exc() |
| 1016 | |
| 1017 | if not success: |
| 1018 | # force the parser and lexer to be rebuilt - we have some problems otherwise |
| 1019 | parser = None |
| 1020 | msg = 'could not parse text: "%s"' % error |
| 1021 | raise ValueError(msg) |
| 1022 | return m |
| 1023 | |
| 1024 | def list_headers(root): |
| 1025 | modules = [] |
| 1026 | support_macros = None |
| 1027 | |
| 1028 | for dirpath, dirnames, filenames in os.walk(root): |
| 1029 | for name in filenames: |
| 1030 | modname = os.path.splitext(name) |
| 1031 | filename = os.path.join(dirpath, name) |
| 1032 | |
| 1033 | if modname[1] == '.spt': |
| 1034 | if name == "obj_perm_sets.spt": |
| 1035 | support_macros = filename |
| 1036 | elif len(re.findall("patterns", modname[0])): |
| 1037 | modules.append((modname[0], filename)) |
| 1038 | elif modname[1] == '.if': |
| 1039 | modules.append((modname[0], filename)) |
| 1040 | |
| 1041 | return (modules, support_macros) |
| 1042 | |
| 1043 | |
| 1044 | def parse_headers(root, output=None, expand=True, debug=False): |
| 1045 | from . import util |
| 1046 | |
| 1047 | headers = refpolicy.Headers() |
| 1048 | |
| 1049 | modules = [] |
| 1050 | support_macros = None |
| 1051 | |
| 1052 | if os.path.isfile(root): |
| 1053 | name = os.path.split(root)[1] |
| 1054 | if name == '': |
| 1055 | raise ValueError("Invalid file name %s" % root) |
| 1056 | modname = os.path.splitext(name) |
| 1057 | modules.append((modname[0], root)) |
| 1058 | all_modules, support_macros = list_headers(defaults.headers()) |
| 1059 | else: |
| 1060 | modules, support_macros = list_headers(root) |
| 1061 | |
| 1062 | if expand and not support_macros: |
| 1063 | raise ValueError("could not find support macros (obj_perm_sets.spt)") |
| 1064 | |
| 1065 | def o(msg): |
| 1066 | if output: |
| 1067 | output.write(msg) |
| 1068 | |
| 1069 | def parse_file(f, module, spt=None): |
| 1070 | global parse_file |
| 1071 | if debug: |
| 1072 | o("parsing file %s\n" % f) |
| 1073 | try: |
| 1074 | fd = open(f) |
| 1075 | txt = fd.read() |
| 1076 | fd.close() |
| 1077 | parse_file = f |
| 1078 | parse(txt, module, spt, debug) |
| 1079 | except IOError as e: |
| 1080 | return |
| 1081 | except ValueError as e: |
| 1082 | raise ValueError("error parsing file %s: %s" % (f, str(e))) |
| 1083 | |
| 1084 | spt = None |
| 1085 | if support_macros: |
| 1086 | o("Parsing support macros (%s): " % support_macros) |
| 1087 | spt = refpolicy.SupportMacros() |
| 1088 | parse_file(support_macros, spt) |
| 1089 | |
| 1090 | headers.children.append(spt) |
| 1091 | |
| 1092 | # FIXME: Total hack - add in can_exec rather than parse the insanity |
| 1093 | # of misc_macros. We are just going to pretend that this is an interface |
| 1094 | # to make the expansion work correctly. |
| 1095 | can_exec = refpolicy.Interface("can_exec") |
| 1096 | av = access.AccessVector(["$1","$2","file","execute_no_trans","open", "read", |
| 1097 | "getattr","lock","execute","ioctl"]) |
| 1098 | |
| 1099 | can_exec.children.append(refpolicy.AVRule(av)) |
| 1100 | headers.children.append(can_exec) |
| 1101 | |
| 1102 | o("done.\n") |
| 1103 | |
| 1104 | if output and not debug: |
| 1105 | status = util.ConsoleProgressBar(sys.stdout, steps=len(modules)) |
| 1106 | status.start("Parsing interface files") |
| 1107 | |
| 1108 | failures = [] |
| 1109 | for x in modules: |
| 1110 | m = refpolicy.Module() |
| 1111 | m.name = x[0] |
| 1112 | try: |
| 1113 | if expand: |
| 1114 | parse_file(x[1], m, spt) |
| 1115 | else: |
| 1116 | parse_file(x[1], m) |
| 1117 | except ValueError as e: |
| 1118 | o(str(e) + "\n") |
| 1119 | failures.append(x[1]) |
| 1120 | continue |
| 1121 | |
| 1122 | headers.children.append(m) |
| 1123 | if output and not debug: |
| 1124 | status.step() |
| 1125 | |
| 1126 | if len(failures): |
| 1127 | o("failed to parse some headers: %s" % ", ".join(failures)) |
| 1128 | |
| 1129 | return headers |